Refactor TypeScript interfaces and types for improved readability and consistency
Gitea Auto Deploy / Deploy-Container (push) Failing after 13m19s

- Reformatted interfaces in changelog.ts, enka.ts, extraData.ts, global.d.ts, index.ts, lightconeDetail.ts, metaData.ts, mics.ts, mocDetail.ts, modelConfig.ts, monsterDetail.ts, peakDetail.ts, pfDetail.ts, psConnect.ts, relicDetail.ts, showcase.ts, srtools.ts, and zod/index.ts for consistent indentation and spacing.
- Added missing imports and interfaces where necessary, such as Invasion in mocDetail.ts and peakDetail.ts.
- Ensured all interfaces are properly structured and maintain a consistent style across the codebase.
This commit is contained in:
2026-08-27 00:38:15 +07:00
parent faafd9aecf
commit 2a36904b43
131 changed files with 16135 additions and 15014 deletions
+1
View File
@@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
.agents/
.history/
/node_modules
/.pnp
+299 -293
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+1
View File
@@ -6,6 +6,7 @@ const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
"node_modules/**",
".next/**",
".history/**",
".agents/**",
"out/**",
"build/**",
"next-env.d.ts",
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly-Tools von Firefly Shelter"
},
"DataPage": {
"skillType": "Fähigkeitstyp",
"skillName": "Fähigkeitsname",
"character": "Charakter",
"id": "ID",
"path": "Pfad",
"rarity": "Seltenheit",
"element": "Element",
"technique": "Technik",
"talent": "Talent",
"basic": "Standardangriff",
"skill": "Fähigkeit",
"ultimate": "Ultimate",
"servant": "Diener",
"damage": "Schaden",
"type": "Typ",
"warrior": "Die Zerstörung",
"knight": "Die Bewahrung",
"mage": "Die Gelehrsamkeit",
"priest": "Der Überfluss",
"rogue": "Die Jagd",
"shaman": "Die Harmonie",
"warlock": "Die Nichtigkeit",
"memory": "Die Erinnerung",
"elation": "Die Freude",
"fire": "Feuer",
"ice": "Eis",
"imaginary": "Imaginär",
"physical": "Physisch",
"quantum": "Quanten",
"thunder": "Blitz",
"wind": "Wind",
"hp": "LP",
"atk": "ANG",
"speed": "GES",
"critRate": "Krit. Rate",
"critDmg": "Krit. SCH",
"breakEffect": "Brucheffekt",
"effectRes": "Effekt-WDS",
"energyRegenerationRate": "Energie-Regenerationsrate",
"effectHitRate": "Effekttrefferquote",
"outgoingHealingBoost": "Ausgehender Heilungsbonus",
"fireDmgBoost": "Feuer-SCH-Bonus",
"iceDmgBoost": "Eis-SCH-Bonus",
"imaginaryDmgBoost": "Imaginär-SCH-Bonus",
"physicalDmgBoost": "Physisch-SCH-Bonus",
"quantumDmgBoost": "Quanten-SCH-Bonus",
"thunderDmgBoost": "Blitz-SCH-Bonus",
"windDmgBoost": "Wind-SCH-Bonus",
"pursued": "Zusätzlicher Schaden",
"true damage": "Wahrer Schaden",
"elationdamage": "Freude-Schaden",
"follow-up": "Folgeangriff-Schaden",
"elemental damage": "Bruch- und Superbruch-Schaden",
"dot": "Schaden über Zeit",
"qte": "QTE-Fähigkeit",
"level": "Stufe",
"relics": "Relikte",
"eidolons": "Eidolons",
"lightcones": "Lichtkegel",
"loadData": "Daten laden",
"exportData": "Daten exportieren",
"connectSetting": "Verbindungseinstellungen",
"connected": "Verbunden",
"unconnected": "Nicht verbunden",
"psConnection": "PS-Verbindung",
"connectionType": "Verbindungstyp",
"status": "Status",
"connectPs": "Mit PS verbinden",
"disconnect": "Trennen",
"other": "Andere",
"freeSr": "FreeSR",
"database": "Datenbank",
"enka": "Enka",
"monsterSetting": "Monstereinstellungen",
"serverUrl": "Server-URL",
"privateType": "Privater Typ",
"local": "Lokal",
"server": "Server",
"username": "Benutzername",
"password": "Passwort",
"placeholderServerUrl": "Server-URL eingeben",
"placeholderUsername": "Benutzernamen eingeben",
"placeholderPassword": "Passwort eingeben",
"connectedSuccess": "Erfolgreich mit PS verbunden",
"connectedFailed": "Verbindung zu PS fehlgeschlagen",
"syncSuccess": "Daten erfolgreich mit PS synchronisiert",
"syncFailed": "Fehler beim Synchronisieren",
"sync": "Synchronisieren",
"importSetting": "Import-Einstellungen",
"profile": "Profil",
"default": "Standard",
"copyProfiles": "Profile kopieren",
"addNewProfile": "Neues Profil hinzufügen",
"createNewProfile": "Neues Profil erstellen",
"editProfile": "Profil bearbeiten",
"placeholderProfileName": "Profilnamen eingeben",
"profileName": "Profilname",
"create": "Erstellen",
"update": "Aktualisieren",
"characterInformation": "Charakterinformationen",
"skills": "Fähigkeiten",
"showcaseCard": "Showcase-Karte",
"comingSoon": "Demnächst",
"characterName": "Charaktername",
"placeholderCharacter": "Charakternamen eingeben",
"characterSettings": "Charaktereinstellungen",
"levelConfiguration": "Stufenkonfiguration",
"characterLevel": "Charakterstufe",
"max": "MAX",
"ultimateEnergy": "Ultimate-Energie",
"currentEnergy": "Aktuelle Energie",
"setTo50": "Auf 50% setzen",
"battleConfiguration": "Kampfkonfiguration",
"useTechnique": "Technik verwenden",
"techniqueNote": "Technikeffekte vor dem Kampf aktivieren",
"enhancement": "Verbesserung",
"enhancementLevel": "Verbesserungsstufe",
"origin": "Ursprung",
"enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei",
"lightconeEquipment": "Lichtkegel-Ausrüstung",
"lightconeSettings": "Lichtkegel-Einstellungen",
"placeholderLevel": "Stufe eingeben",
"superimpositionRank": "Überlagerungsrang",
"ranksNote": "Höhere Ränge bieten stärkere Effekte",
"changeLightcone": "Lichtkegel wechseln",
"removeLightcone": "Lichtkegel entfernen",
"equipLightcone": "Lichtkegel ausrüsten",
"noLightconeEquipped": "Kein Lichtkegel ausgerüstet",
"equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken",
"filter": "Filter",
"selectedCharacters": "Ausgewählte Charaktere",
"selectedProfiles": "Ausgewählte Profile",
"clearAll": "Alles löschen",
"selectAll": "Alles auswählen",
"copy": "Kopieren",
"copied": "Kopiert",
"noAvatarSelected": "Kein Charakter ausgewählt",
"noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt",
"pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus",
"pleaseEnterUid": "Bitte UID eingeben",
"failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten",
"pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus",
"noDataToImport": "Keine Daten zum Importieren",
"pleaseSelectAFile": "Bitte wähle eine Datei",
"fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein",
"importEnkaDataSuccess": "Enka-Daten erfolgreich importiert",
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
"getData": "Daten abrufen",
"import": "Importieren",
"freeSRImport": "FreeSR-Import",
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
"pickAFile": "Datei auswählen",
"lightConeSetting": "Lichtkegel-Einstellung",
"relicMaker": "Relikt-Ersteller",
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
"relicSavedSuccessfully": "Relikt erfolgreich gespeichert",
"mainSettings": "Haupteinstellungen",
"mainStat": "Hauptwert",
"set": "Set",
"pleaseSelectASet": "Bitte wähle ein Set",
"effectBonus": "Effektbonus",
"totalRoll": "Gesamte Aufwertungen",
"randomizeStats": "Werte zufällig",
"randomizeRolls": "Aufwertungen zufällig",
"selectASubStat": "Wähle einen Nebenwert",
"selectASet": "Wähle ein Set",
"selectAMainStat": "Wähle einen Hauptwert",
"save": "Speichern",
"reset": "Zurücksetzen",
"roll": "Aufwertung",
"step": "Schritt",
"memoryOfChaos": "Vergessene Halle",
"pureFiction": "Reine Fiktion",
"apocalypticShadow": "Apokalyptischer Schatten",
"customEnemy": "Benutzerdefinierter Feind",
"simulatedUniverse": "Universum-Simulation",
"floor": "Ebene",
"side": "Seite",
"wave": "Welle",
"stage": "Phase",
"useCycleCount": "Zyklen-Zählung verwenden?",
"useTurbulenceBuff": "Turbulenz-Buff verwenden?",
"firstHalfEnemies": "Gegner erste Hälfte",
"secondHalfEnemies": "Gegner zweite Hälfte",
"firstNodeEnemies": "Gegner Knoten 1",
"secondNodeEnemies": "Gegner Knoten 2",
"thirdNodeEnemies": "Gegner Knoten 3",
"firstNode": "Knoten 1",
"secondNode": "Knoten 2",
"thirdNode": "Knoten 3",
"listEnemies": "Gegnerliste",
"turbulenceBuff": "Turbulenz-Buff",
"noEventSelected": "Kein Ereignis ausgewählt",
"noTurbulenceBuff": "Kein Turbulenz-Buff",
"upper": "Oben",
"lower": "Unten",
"upperToLower": "Oben -> Unten",
"lowerToUpper": "Unten -> Oben",
"selectMOCEvent": "MOC-Ereignis wählen",
"selectPFEvent": "PF-Ereignis wählen",
"selectASEvent": "AS-Ereignis wählen",
"selectCEEvent": "CE-Ereignis wählen",
"selectEvent": "Ereignis wählen",
"selectFloor": "Ebene wählen",
"selectSide": "Seite wählen",
"selectBuff": "Buff wählen",
"selectStage": "Phase wählen",
"previous": "Zurück",
"next": "Weiter",
"noMonstersFound": "Keine Monster gefunden",
"addNewWave": "Neue Welle hinzufügen",
"searchStage": "Phase suchen...",
"noStageFound": "Keine Phase gefunden",
"searchMonster": "Monster suchen...",
"changeRelic": "Relikt wechseln",
"deleteRelic": "Relikt löschen",
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
"setEffects": "Effekte einstellen",
"details": "Details",
"normal": "Standardangriff",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly-Tools von Firefly Shelter"
},
"DataPage": {
"skillType": "Fähigkeitstyp",
"skillName": "Fähigkeitsname",
"character": "Charakter",
"id": "ID",
"path": "Pfad",
"rarity": "Seltenheit",
"element": "Element",
"technique": "Technik",
"talent": "Talent",
"basic": "Standardangriff",
"skill": "Fähigkeit",
"ultimate": "Ultimate",
"servant": "Diener",
"damage": "Schaden",
"type": "Typ",
"warrior": "Die Zerstörung",
"knight": "Die Bewahrung",
"mage": "Die Gelehrsamkeit",
"priest": "Der Überfluss",
"rogue": "Die Jagd",
"shaman": "Die Harmonie",
"warlock": "Die Nichtigkeit",
"memory": "Die Erinnerung",
"elation": "Die Freude",
"fire": "Feuer",
"ice": "Eis",
"imaginary": "Imaginär",
"physical": "Physisch",
"quantum": "Quanten",
"thunder": "Blitz",
"wind": "Wind",
"hp": "LP",
"atk": "ANG",
"speed": "GES",
"critRate": "Krit. Rate",
"critDmg": "Krit. SCH",
"breakEffect": "Brucheffekt",
"effectRes": "Effekt-WDS",
"energyRegenerationRate": "Energie-Regenerationsrate",
"effectHitRate": "Effekttrefferquote",
"outgoingHealingBoost": "Ausgehender Heilungsbonus",
"fireDmgBoost": "Feuer-SCH-Bonus",
"iceDmgBoost": "Eis-SCH-Bonus",
"imaginaryDmgBoost": "Imaginär-SCH-Bonus",
"physicalDmgBoost": "Physisch-SCH-Bonus",
"quantumDmgBoost": "Quanten-SCH-Bonus",
"thunderDmgBoost": "Blitz-SCH-Bonus",
"windDmgBoost": "Wind-SCH-Bonus",
"pursued": "Zusätzlicher Schaden",
"true damage": "Wahrer Schaden",
"elationdamage": "Freude-Schaden",
"follow-up": "Folgeangriff-Schaden",
"elemental damage": "Bruch- und Superbruch-Schaden",
"dot": "Schaden über Zeit",
"qte": "QTE-Fähigkeit",
"level": "Stufe",
"relics": "Relikte",
"eidolons": "Eidolons",
"lightcones": "Lichtkegel",
"loadData": "Daten laden",
"exportData": "Daten exportieren",
"connectSetting": "Verbindungseinstellungen",
"connected": "Verbunden",
"unconnected": "Nicht verbunden",
"psConnection": "PS-Verbindung",
"connectionType": "Verbindungstyp",
"status": "Status",
"connectPs": "Mit PS verbinden",
"disconnect": "Trennen",
"other": "Andere",
"freeSr": "FreeSR",
"database": "Datenbank",
"enka": "Enka",
"monsterSetting": "Monstereinstellungen",
"serverUrl": "Server-URL",
"privateType": "Privater Typ",
"local": "Lokal",
"server": "Server",
"username": "Benutzername",
"password": "Passwort",
"placeholderServerUrl": "Server-URL eingeben",
"placeholderUsername": "Benutzernamen eingeben",
"placeholderPassword": "Passwort eingeben",
"connectedSuccess": "Erfolgreich mit PS verbunden",
"connectedFailed": "Verbindung zu PS fehlgeschlagen",
"syncSuccess": "Daten erfolgreich mit PS synchronisiert",
"syncFailed": "Fehler beim Synchronisieren",
"sync": "Synchronisieren",
"importSetting": "Import-Einstellungen",
"profile": "Profil",
"default": "Standard",
"copyProfiles": "Profile kopieren",
"addNewProfile": "Neues Profil hinzufügen",
"createNewProfile": "Neues Profil erstellen",
"editProfile": "Profil bearbeiten",
"placeholderProfileName": "Profilnamen eingeben",
"profileName": "Profilname",
"create": "Erstellen",
"update": "Aktualisieren",
"characterInformation": "Charakterinformationen",
"skills": "Fähigkeiten",
"showcaseCard": "Showcase-Karte",
"comingSoon": "Demnächst",
"characterName": "Charaktername",
"placeholderCharacter": "Charakternamen eingeben",
"characterSettings": "Charaktereinstellungen",
"levelConfiguration": "Stufenkonfiguration",
"characterLevel": "Charakterstufe",
"max": "MAX",
"ultimateEnergy": "Ultimate-Energie",
"currentEnergy": "Aktuelle Energie",
"setTo50": "Auf 50% setzen",
"battleConfiguration": "Kampfkonfiguration",
"useTechnique": "Technik verwenden",
"techniqueNote": "Technikeffekte vor dem Kampf aktivieren",
"enhancement": "Verbesserung",
"enhancementLevel": "Verbesserungsstufe",
"origin": "Ursprung",
"enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei",
"lightconeEquipment": "Lichtkegel-Ausrüstung",
"lightconeSettings": "Lichtkegel-Einstellungen",
"placeholderLevel": "Stufe eingeben",
"superimpositionRank": "Überlagerungsrang",
"ranksNote": "Höhere Ränge bieten stärkere Effekte",
"changeLightcone": "Lichtkegel wechseln",
"removeLightcone": "Lichtkegel entfernen",
"equipLightcone": "Lichtkegel ausrüsten",
"noLightconeEquipped": "Kein Lichtkegel ausgerüstet",
"equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken",
"filter": "Filter",
"selectedCharacters": "Ausgewählte Charaktere",
"selectedProfiles": "Ausgewählte Profile",
"clearAll": "Alles löschen",
"selectAll": "Alles auswählen",
"copy": "Kopieren",
"copied": "Kopiert",
"noAvatarSelected": "Kein Charakter ausgewählt",
"noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt",
"pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus",
"pleaseEnterUid": "Bitte UID eingeben",
"failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten",
"pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus",
"noDataToImport": "Keine Daten zum Importieren",
"pleaseSelectAFile": "Bitte wähle eine Datei",
"fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein",
"importEnkaDataSuccess": "Enka-Daten erfolgreich importiert",
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
"getData": "Daten abrufen",
"import": "Importieren",
"freeSRImport": "FreeSR-Import",
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
"pickAFile": "Datei auswählen",
"lightConeSetting": "Lichtkegel-Einstellung",
"relicMaker": "Relikt-Ersteller",
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
"relicSavedSuccessfully": "Relikt erfolgreich gespeichert",
"mainSettings": "Haupteinstellungen",
"mainStat": "Hauptwert",
"set": "Set",
"pleaseSelectASet": "Bitte wähle ein Set",
"effectBonus": "Effektbonus",
"totalRoll": "Gesamte Aufwertungen",
"randomizeStats": "Werte zufällig",
"randomizeRolls": "Aufwertungen zufällig",
"selectASubStat": "Wähle einen Nebenwert",
"selectASet": "Wähle ein Set",
"selectAMainStat": "Wähle einen Hauptwert",
"save": "Speichern",
"reset": "Zurücksetzen",
"roll": "Aufwertung",
"step": "Schritt",
"memoryOfChaos": "Vergessene Halle",
"pureFiction": "Reine Fiktion",
"apocalypticShadow": "Apokalyptischer Schatten",
"customEnemy": "Benutzerdefinierter Feind",
"simulatedUniverse": "Universum-Simulation",
"floor": "Ebene",
"side": "Seite",
"wave": "Welle",
"stage": "Phase",
"useCycleCount": "Zyklen-Zählung verwenden?",
"useTurbulenceBuff": "Turbulenz-Buff verwenden?",
"firstHalfEnemies": "Gegner erste Hälfte",
"secondHalfEnemies": "Gegner zweite Hälfte",
"firstNodeEnemies": "Gegner Knoten 1",
"secondNodeEnemies": "Gegner Knoten 2",
"thirdNodeEnemies": "Gegner Knoten 3",
"firstNode": "Knoten 1",
"secondNode": "Knoten 2",
"thirdNode": "Knoten 3",
"listEnemies": "Gegnerliste",
"turbulenceBuff": "Turbulenz-Buff",
"noEventSelected": "Kein Ereignis ausgewählt",
"noTurbulenceBuff": "Kein Turbulenz-Buff",
"upper": "Oben",
"lower": "Unten",
"upperToLower": "Oben -> Unten",
"lowerToUpper": "Unten -> Oben",
"selectMOCEvent": "MOC-Ereignis wählen",
"selectPFEvent": "PF-Ereignis wählen",
"selectASEvent": "AS-Ereignis wählen",
"selectCEEvent": "CE-Ereignis wählen",
"selectEvent": "Ereignis wählen",
"selectFloor": "Ebene wählen",
"selectSide": "Seite wählen",
"selectBuff": "Buff wählen",
"selectStage": "Phase wählen",
"previous": "Zurück",
"next": "Weiter",
"noMonstersFound": "Keine Monster gefunden",
"addNewWave": "Neue Welle hinzufügen",
"searchStage": "Phase suchen...",
"noStageFound": "Keine Phase gefunden",
"searchMonster": "Monster suchen...",
"changeRelic": "Relikt wechseln",
"deleteRelic": "Relikt löschen",
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
"setEffects": "Effekte einstellen",
"details": "Details",
"normal": "Standardangriff",
"bpskill": "Fähigkeit",
"maze": "Technik",
"ultra": "Ultimate",
"assist": "Assist-Fähigkeit",
"servantskill": "Memosprite-Fähigkeit",
"severaltalent": "Memosprite-Talent",
"singleattack": "Einzelangriff",
"enhance": "Verbessern",
"summon": "Beschwören",
"mazeattack": "Technik-Angriff",
"blast": "Explosion",
"restore": "Wiederherstellen",
"support": "Unterstützung",
"aoeattack": "Flächenangriff",
"impair": "Beeinträchtigen",
"bounce": "Abprallen",
"active": "Aktiv",
"defence": "Verteidigung",
"inactive": "Inaktiv",
"maxAll": "Alles maximieren",
"maxAllSuccess": "Fähigkeiten erfolgreich maximiert.",
"maxAllFailed": "Fehler beim Maximieren.",
"noRelicEquipped": "Kein Relikt ausgerüstet",
"anomalyArbitration": "Anomalie-Schiedsgericht",
"normalMode": "Normaler Modus",
"hardMode": "Schwerer Modus",
"selectPEAKEvent": "PEAK-Ereignis wählen",
"mode": "Modus",
"selectMode": "Modus wählen",
"rollBack": "Rückgängig",
"upRoll": "Aufwertung hoch",
"downRoll": "Aufwertung runter",
"actions": "Aktionen",
"avatars": "Avatare",
"quickView": "Schnellansicht",
"extraSetting": "Extra-Einstellungen",
"disableCensorship": "Zensur deaktivieren",
"hideUI": "UI verstecken",
"theoryCraftMode": "Theorycraft-Modus",
"cycleCount": "Zyklen-Anzahl",
"pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen",
"subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Mehrpfad-Charakter",
"mainPath": "Hauptpfad",
"march7Path": "Pfad 7. März",
"challenge": "Herausforderung",
"skipNode": "Knoten überspringen",
"disableSkip": "Überspringen deaktivieren",
"skipNode1": "Knoten 1 überspringen",
"skipNode2": "Knoten 2 überspringen",
"extraFeatures": "Zusatzfunktionen",
"detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.",
"detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.",
"detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.",
"detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.",
"detailDisableCensorship": "Deaktiviert die Zensur im Spiel.",
"detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Liste Zusatzeffekte",
"extra": "Extra",
"customLineup": "Benutzerdefinierte Aufstellung"
}
"severaltalent": "Memosprite-Talent",
"singleattack": "Einzelangriff",
"enhance": "Verbessern",
"summon": "Beschwören",
"mazeattack": "Technik-Angriff",
"blast": "Explosion",
"restore": "Wiederherstellen",
"support": "Unterstützung",
"aoeattack": "Flächenangriff",
"impair": "Beeinträchtigen",
"bounce": "Abprallen",
"active": "Aktiv",
"defence": "Verteidigung",
"inactive": "Inaktiv",
"maxAll": "Alles maximieren",
"maxAllSuccess": "Fähigkeiten erfolgreich maximiert.",
"maxAllFailed": "Fehler beim Maximieren.",
"noRelicEquipped": "Kein Relikt ausgerüstet",
"anomalyArbitration": "Anomalie-Schiedsgericht",
"normalMode": "Normaler Modus",
"hardMode": "Schwerer Modus",
"selectPEAKEvent": "PEAK-Ereignis wählen",
"mode": "Modus",
"selectMode": "Modus wählen",
"rollBack": "Rückgängig",
"upRoll": "Aufwertung hoch",
"downRoll": "Aufwertung runter",
"actions": "Aktionen",
"avatars": "Avatare",
"quickView": "Schnellansicht",
"extraSetting": "Extra-Einstellungen",
"disableCensorship": "Zensur deaktivieren",
"hideUI": "UI verstecken",
"theoryCraftMode": "Theorycraft-Modus",
"cycleCount": "Zyklen-Anzahl",
"pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen",
"subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Mehrpfad-Charakter",
"mainPath": "Hauptpfad",
"march7Path": "Pfad 7. März",
"challenge": "Herausforderung",
"skipNode": "Knoten überspringen",
"disableSkip": "Überspringen deaktivieren",
"skipNode1": "Knoten 1 überspringen",
"skipNode2": "Knoten 2 überspringen",
"extraFeatures": "Zusatzfunktionen",
"detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.",
"detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.",
"detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.",
"detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.",
"detailDisableCensorship": "Deaktiviert die Zensur im Spiel.",
"detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Liste Zusatzeffekte",
"extra": "Extra",
"customLineup": "Benutzerdefinierte Aufstellung",
"def": "VTD",
"language": "Sprache"
}
}
+293 -291
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Skill Type",
"skillName": "Skill Name",
"character": "Character",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Element",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Type",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Thunder",
"wind": "Wind",
"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",
"elationdamage": "Elation damage",
"follow-up": "Follow-up Damage",
"elemental damage": "Break and Super break damage",
"dot": "Damage over time ",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relics",
"eidolons": "Eidolons",
"lightcones": "Lightcones",
"loadData": "Load data",
"exportData": "Export data",
"connectSetting": "Connection Setting",
"connected": "Connected",
"unconnected": "Unconnected",
"psConnection": "PS Connection",
"connectionType": "Connection Type",
"status": "Status",
"connectPs": "Connect PS",
"disconnect": "Disconnect",
"other": "Other",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Monster Setting",
"serverUrl": "Server URL",
"privateType": "Private Type",
"local": "Local",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Enter server URL",
"placeholderUsername": "Enter username",
"placeholderPassword": "Enter password",
"connectedSuccess": "Connected to PS successfully",
"connectedFailed": "Failed to connect to PS",
"syncSuccess": "Synced data to PS successfully",
"syncFailed": "Failed to sync data to PS",
"sync": "Sync",
"importSetting": "Import Setting",
"profile": "Profile",
"default": "Default",
"copyProfiles": "Copy profiles",
"addNewProfile": "Add new profile",
"createNewProfile": "Create new profile",
"editProfile": "Edit profile",
"placeholderProfileName": "Enter profile name",
"profileName": "Profile name",
"create": "Create",
"update": "Update",
"characterInformation": "Character Information",
"skills": "Skills",
"showcaseCard": "Showcase Card",
"comingSoon": "Coming soon",
"characterName": "Character name",
"placeholderCharacter": "Enter character name",
"characterSettings": "Character Settings",
"levelConfiguration": "Level Configuration",
"characterLevel": "Character Level",
"max": "MAX",
"ultimateEnergy": "Ultimate Energy",
"currentEnergy": "Current Energy",
"setTo50": "Set to 50%",
"battleConfiguration": "Battle Configuration",
"useTechnique": "Use Technique",
"techniqueNote": "Enable pre-battle technique effects",
"enhancement": "Enhancement",
"enhancementLevel": "Enhancement Level",
"origin": "Origin",
"enhancedNote": "Higher enhanced unlock additional abilities",
"lightconeEquipment": "Lightcone Equipment",
"lightconeSettings": "Lightcone Settings",
"placeholderLevel": "Enter level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Higher ranks provide stronger effects",
"changeLightcone": "Change Lightcone",
"removeLightcone": "Remove Lightcone",
"equipLightcone": "Equip Lightcone",
"noLightconeEquipped": "No Lightcone Equipped",
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
"filter": "Filter",
"selectedCharacters": "Selected Characters",
"selectedProfiles": "Selected Profiles",
"clearAll": "Clear All",
"selectAll": "Select All",
"copy": "Copy",
"copied": "Copied",
"noAvatarSelected": "No avatar selected",
"noAvatarToCopySelected": "No avatar to copy selected",
"pleaseSelectAtLeastOneProfile": "Please select at least one profile",
"pleaseEnterUid": "Please enter UID",
"failedToFetchEnkaData": "Failed to fetch enka data",
"pleaseSelectAtLeastOneCharacter": "Please select at least one character",
"noDataToImport": "No data to import",
"pleaseSelectAFile": "Please select a file",
"fileMustBeAValidJsonFile": "File must be a valid json file",
"importEnkaDataSuccess": "Import Enka data success",
"importFreeSRDataSuccess": "Import FreeSR data success",
"importDatabaseSuccess": "Import database success",
"getData": "Get Data",
"import": "Import",
"freeSRImport": "FreeSR Import",
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
"pickAFile": "Pick a file",
"lightConeSetting": "LightCone Setting",
"relicMaker": "Relic Maker",
"pleaseSelectAllOptions": "Please select all options",
"relicSavedSuccessfully": "Relic saved successfully",
"mainSettings": "Main Settings",
"mainStat": "Main Stat",
"set": "Set",
"pleaseSelectASet": "Please select a set",
"effectBonus": "Effect Bonus",
"totalRoll": "Total Roll",
"randomizeStats": "Randomize Stats",
"randomizeRolls": "Randomize Rolls",
"selectASubStat": "Select a sub stat",
"selectASet": "Select a set",
"selectAMainStat": "Select a main stat",
"save": "Save",
"reset": "Reset",
"roll": "Roll",
"step": "Step",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Custom Enemy",
"simulatedUniverse": "Simulated Universe",
"floor": "Floor",
"side": "Side",
"wave": "Wave",
"stage": "Stage",
"useCycleCount": "Use cycle count?",
"useTurbulenceBuff": "Use turbulence buff?",
"firstHalfEnemies": "First half enemies",
"secondHalfEnemies": "Second half enemies",
"firstNodeEnemies": "First node enemies",
"secondNodeEnemies": "Second node enemies",
"thirdNodeEnemies": "Third node enemies",
"firstNode": "First Node",
"secondNode": "Second Node",
"thirdNode": "Third Node",
"listEnemies": "List enemies",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "No event selected",
"noTurbulenceBuff": "No Turbulence Buff",
"upper": "Upper",
"lower": "Lower",
"upperToLower": "Upper -> Lower",
"lowerToUpper": "Lower -> Upper",
"selectMOCEvent": "Select MOC Event",
"selectPFEvent": "Select PF Event",
"selectASEvent": "Select AS Event",
"selectCEEvent": "Select CE Event",
"selectEvent": "Select Event",
"selectFloor": "Select a Floor",
"selectSide": "Select a Side",
"selectBuff": "Select a Buff",
"selectStage": "Select a Stage",
"previous": "Previous",
"next": "Next",
"noMonstersFound": "No monsters found",
"addNewWave": "Add New Wave",
"searchStage": "Search stage...",
"noStageFound": "No stage found",
"searchMonster": "Search monster...",
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects",
"details": "Details",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"assist": "Assist Skill",
"servantskill": "Memosprite Skill",
"severaltalent": "Memosprite Talent",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Active",
"defence": "Defence",
"inactive": "Inactive",
"maxAll": "Max All",
"maxAllSuccess": "Successfully set skill level to max.",
"maxAllFailed": "Failed to set skill level to max.",
"noRelicEquipped": "No relic equipped",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Normal Mode",
"hardMode": "Hard Mode",
"selectPEAKEvent": "Select PEAK Event",
"mode": "Mode",
"selectMode": "Select a mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Quick View",
"extraSetting": "Extra Settings",
"disableCensorship": "Disable Censorship",
"hideUI": "Hide UI",
"theoryCraftMode": "Theorycraft Mode",
"cycleCount": "Cycle Count",
"pleaseSelectAllSubStats": "Please select all sub stats",
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Multipath Character",
"mainPath": "Main Path",
"march7Path": "March 7 Path",
"challenge": "Challenge",
"skipNode": "Skip Node",
"disableSkip": "Disable skip",
"skipNode1": "Skip node 1",
"skipNode2": "Skip node 2",
"extraFeatures": "Extra Features",
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
"detailHiddenUi": "Enabling this feature will hide the game UI.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"detailMultipathCharacter": "Allows changing the Path of certain characters.",
"trailblazer": "Trailblazer",
"listExtraEffect": "List Extra Effect",
"extra": "Extra",
"customLineup": "Custom Lineup"
}
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Skill Type",
"skillName": "Skill Name",
"character": "Character",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Element",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Type",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Thunder",
"wind": "Wind",
"hp": "Hp",
"atk": "Atk",
"def": "Def",
"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",
"elationdamage": "Elation damage",
"follow-up": "Follow-up Damage",
"elemental damage": "Break and Super break damage",
"dot": "Damage over time ",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relics",
"eidolons": "Eidolons",
"lightcones": "Lightcones",
"loadData": "Load data",
"exportData": "Export data",
"connectSetting": "Connection Setting",
"connected": "Connected",
"unconnected": "Unconnected",
"psConnection": "PS Connection",
"connectionType": "Connection Type",
"status": "Status",
"connectPs": "Connect PS",
"disconnect": "Disconnect",
"other": "Other",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Monster Setting",
"serverUrl": "Server URL",
"privateType": "Private Type",
"local": "Local",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Enter server URL",
"placeholderUsername": "Enter username",
"placeholderPassword": "Enter password",
"connectedSuccess": "Connected to PS successfully",
"connectedFailed": "Failed to connect to PS",
"syncSuccess": "Synced data to PS successfully",
"syncFailed": "Failed to sync data to PS",
"sync": "Sync",
"importSetting": "Import Setting",
"profile": "Profile",
"default": "Default",
"copyProfiles": "Copy profiles",
"addNewProfile": "Add new profile",
"createNewProfile": "Create new profile",
"editProfile": "Edit profile",
"placeholderProfileName": "Enter profile name",
"profileName": "Profile name",
"create": "Create",
"update": "Update",
"characterInformation": "Character Information",
"skills": "Skills",
"showcaseCard": "Showcase Card",
"comingSoon": "Coming soon",
"characterName": "Character name",
"placeholderCharacter": "Enter character name",
"characterSettings": "Character Settings",
"levelConfiguration": "Level Configuration",
"characterLevel": "Character Level",
"max": "MAX",
"ultimateEnergy": "Ultimate Energy",
"currentEnergy": "Current Energy",
"setTo50": "Set to 50%",
"battleConfiguration": "Battle Configuration",
"useTechnique": "Use Technique",
"techniqueNote": "Enable pre-battle technique effects",
"enhancement": "Enhancement",
"enhancementLevel": "Enhancement Level",
"origin": "Origin",
"enhancedNote": "Higher enhanced unlock additional abilities",
"lightconeEquipment": "Lightcone Equipment",
"lightconeSettings": "Lightcone Settings",
"placeholderLevel": "Enter level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Higher ranks provide stronger effects",
"changeLightcone": "Change Lightcone",
"removeLightcone": "Remove Lightcone",
"equipLightcone": "Equip Lightcone",
"noLightconeEquipped": "No Lightcone Equipped",
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
"filter": "Filter",
"selectedCharacters": "Selected Characters",
"selectedProfiles": "Selected Profiles",
"clearAll": "Clear All",
"selectAll": "Select All",
"copy": "Copy",
"copied": "Copied",
"noAvatarSelected": "No avatar selected",
"noAvatarToCopySelected": "No avatar to copy selected",
"pleaseSelectAtLeastOneProfile": "Please select at least one profile",
"pleaseEnterUid": "Please enter UID",
"failedToFetchEnkaData": "Failed to fetch enka data",
"pleaseSelectAtLeastOneCharacter": "Please select at least one character",
"noDataToImport": "No data to import",
"pleaseSelectAFile": "Please select a file",
"fileMustBeAValidJsonFile": "File must be a valid json file",
"importEnkaDataSuccess": "Import Enka data success",
"importFreeSRDataSuccess": "Import FreeSR data success",
"importDatabaseSuccess": "Import database success",
"getData": "Get Data",
"import": "Import",
"freeSRImport": "FreeSR Import",
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
"pickAFile": "Pick a file",
"lightConeSetting": "LightCone Setting",
"relicMaker": "Relic Maker",
"pleaseSelectAllOptions": "Please select all options",
"relicSavedSuccessfully": "Relic saved successfully",
"mainSettings": "Main Settings",
"mainStat": "Main Stat",
"set": "Set",
"pleaseSelectASet": "Please select a set",
"effectBonus": "Effect Bonus",
"totalRoll": "Total Roll",
"randomizeStats": "Randomize Stats",
"randomizeRolls": "Randomize Rolls",
"selectASubStat": "Select a sub stat",
"selectASet": "Select a set",
"selectAMainStat": "Select a main stat",
"save": "Save",
"reset": "Reset",
"roll": "Roll",
"step": "Step",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Custom Enemy",
"simulatedUniverse": "Simulated Universe",
"floor": "Floor",
"side": "Side",
"wave": "Wave",
"stage": "Stage",
"useCycleCount": "Use cycle count?",
"useTurbulenceBuff": "Use turbulence buff?",
"firstHalfEnemies": "First half enemies",
"secondHalfEnemies": "Second half enemies",
"firstNodeEnemies": "First node enemies",
"secondNodeEnemies": "Second node enemies",
"thirdNodeEnemies": "Third node enemies",
"firstNode": "First Node",
"secondNode": "Second Node",
"thirdNode": "Third Node",
"listEnemies": "List enemies",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "No event selected",
"noTurbulenceBuff": "No Turbulence Buff",
"upper": "Upper",
"lower": "Lower",
"upperToLower": "Upper -> Lower",
"lowerToUpper": "Lower -> Upper",
"selectMOCEvent": "Select MOC Event",
"selectPFEvent": "Select PF Event",
"selectASEvent": "Select AS Event",
"selectCEEvent": "Select CE Event",
"selectEvent": "Select Event",
"selectFloor": "Select a Floor",
"selectSide": "Select a Side",
"selectBuff": "Select a Buff",
"selectStage": "Select a Stage",
"previous": "Previous",
"next": "Next",
"noMonstersFound": "No monsters found",
"addNewWave": "Add New Wave",
"searchStage": "Search stage...",
"noStageFound": "No stage found",
"searchMonster": "Search monster...",
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects",
"details": "Details",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"assist": "Assist Skill",
"servantskill": "Memosprite Skill",
"severaltalent": "Memosprite Talent",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Active",
"defence": "Defence",
"inactive": "Inactive",
"maxAll": "Max All",
"maxAllSuccess": "Successfully set skill level to max.",
"maxAllFailed": "Failed to set skill level to max.",
"noRelicEquipped": "No relic equipped",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Normal Mode",
"hardMode": "Hard Mode",
"selectPEAKEvent": "Select PEAK Event",
"mode": "Mode",
"selectMode": "Select a mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Quick View",
"extraSetting": "Extra Settings",
"disableCensorship": "Disable Censorship",
"hideUI": "Hide UI",
"theoryCraftMode": "Theorycraft Mode",
"cycleCount": "Cycle Count",
"pleaseSelectAllSubStats": "Please select all sub stats",
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Multipath Character",
"mainPath": "Main Path",
"march7Path": "March 7 Path",
"challenge": "Challenge",
"skipNode": "Skip Node",
"disableSkip": "Disable skip",
"skipNode1": "Skip node 1",
"skipNode2": "Skip node 2",
"extraFeatures": "Extra Features",
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
"detailHiddenUi": "Enabling this feature will hide the game UI.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"detailMultipathCharacter": "Allows changing the Path of certain characters.",
"trailblazer": "Trailblazer",
"listExtraEffect": "List Extra Effect",
"extra": "Extra",
"customLineup": "Custom Lineup",
"language": "Language"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Herramientas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de habilidad",
"skillName": "Nombre de la habilidad",
"character": "Personaje",
"id": "Id",
"path": "Vía",
"rarity": "Rareza",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Habilidad",
"ultimate": "Habilidad Definitiva",
"servant": "Mnemoduende",
"damage": "Daño",
"type": "Tipo",
"warrior": "La Destrucción",
"knight": "La Conservación",
"mage": "La Erudición",
"priest": "La Abundancia",
"rogue": "La Cacería",
"shaman": "La Armonía",
"warlock": "La Nihilidad",
"memory": "La Reminiscencia",
"elation": "La Exultación",
"fire": "Fuego",
"ice": "Hielo",
"imaginary": "Imaginario",
"physical": "Físico",
"quantum": "Cuántico",
"thunder": "Rayo",
"wind": "Viento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Prob. CRIT",
"critDmg": "Daño CRIT",
"breakEffect": "Efecto de Ruptura",
"effectRes": "RES a Efecto",
"energyRegenerationRate": "Recuperación de Energía",
"effectHitRate": "Acierto de Efecto",
"outgoingHealingBoost": "Bono de Curación",
"fireDmgBoost": "Aumento de Daño de Fuego",
"iceDmgBoost": "Aumento de Daño de Hielo",
"imaginaryDmgBoost": "Aumento de Daño Imaginario",
"physicalDmgBoost": "Aumento de Daño Físico",
"quantumDmgBoost": "Aumento de Daño Cuántico",
"thunderDmgBoost": "Aumento de Daño de Rayo",
"windDmgBoost": "Aumento de Daño de Viento",
"pursued": "Daño adicional",
"true damage": "Daño verdadero",
"elationdamage": "Daño de Exultación",
"follow-up": "Daño de Ataque Adicional",
"elemental damage": "Daño de Ruptura y Superruptura",
"dot": "Daño con el tiempo",
"qte": "Habilidad QTE",
"level": "Nivel",
"relics": "Artefactos",
"eidolons": "Eidolones",
"lightcones": "Conos de Luz",
"loadData": "Cargar datos",
"exportData": "Exportar datos",
"connectSetting": "Ajustes de conexión",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexión PS",
"connectionType": "Tipo de conexión",
"status": "Estado",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Otro",
"freeSr": "FreeSR",
"database": "Base de datos",
"enka": "Enka",
"monsterSetting": "Ajuste de Monstruos",
"serverUrl": "URL del Servidor",
"privateType": "Tipo privado",
"local": "Local",
"server": "Servidor",
"username": "Usuario",
"password": "Contraseña",
"placeholderServerUrl": "Introducir URL del servidor",
"placeholderUsername": "Introducir usuario",
"placeholderPassword": "Introducir contraseña",
"connectedSuccess": "Conectado a PS exitosamente",
"connectedFailed": "Fallo al conectar a PS",
"syncSuccess": "Datos sincronizados a PS exitosamente",
"syncFailed": "Fallo al sincronizar datos a PS",
"sync": "Sincronizar",
"importSetting": "Ajustes de Importación",
"profile": "Perfil",
"default": "Por defecto",
"copyProfiles": "Copiar perfiles",
"addNewProfile": "Añadir nuevo perfil",
"createNewProfile": "Crear nuevo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Introducir nombre de perfil",
"profileName": "Nombre del perfil",
"create": "Crear",
"update": "Actualizar",
"characterInformation": "Información del personaje",
"skills": "Habilidades",
"showcaseCard": "Tarjeta de presentación del personaje",
"comingSoon": "Próximamente",
"characterName": "Nombre del personaje",
"placeholderCharacter": "Introducir nombre del personaje",
"characterSettings": "Ajustes del Personaje",
"levelConfiguration": "Configuración de Nivel",
"characterLevel": "Nivel de Personaje",
"max": "MÁX",
"ultimateEnergy": "Energía de Habilidad Definitiva",
"currentEnergy": "Energía al inicio del combate",
"setTo50": "Ajustar al 50%",
"battleConfiguration": "Configuración de Batalla",
"useTechnique": "Usar Técnica",
"techniqueNote": "Activar efectos de técnica pre-batalla",
"enhancement": "Mejora",
"enhancementLevel": "Nivel de Mejora",
"origin": "Original",
"enhancedNote": "Niveles superiores desbloquean más habilidades",
"lightconeEquipment": "Equipamiento de Cono de Luz",
"lightconeSettings": "Ajustes de Cono de Luz",
"placeholderLevel": "Introducir nivel",
"superimpositionRank": "Rango de Superposición",
"ranksNote": "Rangos mayores ofrecen efectos más fuertes",
"changeLightcone": "Cambiar Cono de Luz",
"removeLightcone": "Quitar Cono de Luz",
"equipLightcone": "Equipar Cono de Luz",
"noLightconeEquipped": "Sin Cono de Luz",
"equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje",
"filter": "Filtro",
"selectedCharacters": "Personajes Seleccionados",
"selectedProfiles": "Perfiles Seleccionados",
"clearAll": "Limpiar Todo",
"selectAll": "Seleccionar Todo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Ningún personaje seleccionado",
"noAvatarToCopySelected": "Ningún personaje seleccionado para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil",
"pleaseEnterUid": "Por favor, introduce UID",
"failedToFetchEnkaData": "Fallo al obtener datos de Enka",
"pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje",
"noDataToImport": "Sin datos para importar",
"pleaseSelectAFile": "Por favor, selecciona un archivo",
"fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido",
"importEnkaDataSuccess": "Datos de Enka importados con éxito",
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
"importDatabaseSuccess": "Base de datos importada con éxito",
"getData": "Obtener Datos",
"import": "Importar",
"freeSRImport": "Importar FreeSR",
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
"pickAFile": "Elegir un archivo",
"lightConeSetting": "Ajuste de Cono de Luz",
"relicMaker": "Creador de Artefactos",
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
"relicSavedSuccessfully": "Artefacto guardado exitosamente",
"mainSettings": "Ajustes Principales",
"mainStat": "Estadística Principal",
"set": "Conjunto",
"pleaseSelectASet": "Por favor, selecciona un conjunto",
"effectBonus": "Bono de Efecto",
"totalRoll": "Total de Mejoras",
"randomizeStats": "Estadísticas Aleatorias",
"randomizeRolls": "Mejoras Aleatorias",
"selectASubStat": "Seleccionar estadística secundaria",
"selectASet": "Seleccionar conjunto",
"selectAMainStat": "Seleccionar estadística principal",
"save": "Guardar",
"reset": "Reiniciar",
"roll": "Mejora",
"step": "Paso",
"memoryOfChaos": "Memoria del Caos",
"pureFiction": "Pura Ficción",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Enemigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Piso",
"side": "Lado",
"wave": "Oleada",
"stage": "Etapa",
"useCycleCount": "¿Usar conteo de ciclos?",
"useTurbulenceBuff": "¿Usar buff de turbulencia?",
"firstHalfEnemies": "Enemigos primera mitad",
"secondHalfEnemies": "Enemigos segunda mitad",
"firstNodeEnemies": "Enemigos del nodo 1",
"secondNodeEnemies": "Enemigos del nodo 2",
"thirdNodeEnemies": "Enemigos del nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de enemigos",
"turbulenceBuff": "Buff de Turbulencia",
"noEventSelected": "Ningún evento seleccionado",
"noTurbulenceBuff": "Sin Buff de Turbulencia",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Seleccionar evento de MOC",
"selectPFEvent": "Seleccionar evento de PF",
"selectASEvent": "Seleccionar evento de AS",
"selectCEEvent": "Seleccionar evento de CE",
"selectEvent": "Seleccionar Evento",
"selectFloor": "Seleccionar Piso",
"selectSide": "Seleccionar Lado",
"selectBuff": "Seleccionar Buff",
"selectStage": "Seleccionar Etapa",
"previous": "Anterior",
"next": "Siguiente",
"noMonstersFound": "No se encontraron monstruos",
"addNewWave": "Añadir Nueva Oleada",
"searchStage": "Buscar etapa...",
"noStageFound": "No se encontró etapa",
"searchMonster": "Buscar monstruo...",
"changeRelic": "Cambiar artefacto",
"deleteRelic": "Borrar artefacto",
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
"setEffects": "Configurar Efectos",
"details": "Detalles",
"normal": "Ataque Básico",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Herramientas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de habilidad",
"skillName": "Nombre de la habilidad",
"character": "Personaje",
"id": "Id",
"path": "Vía",
"rarity": "Rareza",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Habilidad",
"ultimate": "Habilidad Definitiva",
"servant": "Mnemoduende",
"damage": "Daño",
"type": "Tipo",
"warrior": "La Destrucción",
"knight": "La Conservación",
"mage": "La Erudición",
"priest": "La Abundancia",
"rogue": "La Cacería",
"shaman": "La Armonía",
"warlock": "La Nihilidad",
"memory": "La Reminiscencia",
"elation": "La Exultación",
"fire": "Fuego",
"ice": "Hielo",
"imaginary": "Imaginario",
"physical": "Físico",
"quantum": "Cuántico",
"thunder": "Rayo",
"wind": "Viento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Prob. CRIT",
"critDmg": "Daño CRIT",
"breakEffect": "Efecto de Ruptura",
"effectRes": "RES a Efecto",
"energyRegenerationRate": "Recuperación de Energía",
"effectHitRate": "Acierto de Efecto",
"outgoingHealingBoost": "Bono de Curación",
"fireDmgBoost": "Aumento de Daño de Fuego",
"iceDmgBoost": "Aumento de Daño de Hielo",
"imaginaryDmgBoost": "Aumento de Daño Imaginario",
"physicalDmgBoost": "Aumento de Daño Físico",
"quantumDmgBoost": "Aumento de Daño Cuántico",
"thunderDmgBoost": "Aumento de Daño de Rayo",
"windDmgBoost": "Aumento de Daño de Viento",
"pursued": "Daño adicional",
"true damage": "Daño verdadero",
"elationdamage": "Daño de Exultación",
"follow-up": "Daño de Ataque Adicional",
"elemental damage": "Daño de Ruptura y Superruptura",
"dot": "Daño con el tiempo",
"qte": "Habilidad QTE",
"level": "Nivel",
"relics": "Artefactos",
"eidolons": "Eidolones",
"lightcones": "Conos de Luz",
"loadData": "Cargar datos",
"exportData": "Exportar datos",
"connectSetting": "Ajustes de conexión",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexión PS",
"connectionType": "Tipo de conexión",
"status": "Estado",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Otro",
"freeSr": "FreeSR",
"database": "Base de datos",
"enka": "Enka",
"monsterSetting": "Ajuste de Monstruos",
"serverUrl": "URL del Servidor",
"privateType": "Tipo privado",
"local": "Local",
"server": "Servidor",
"username": "Usuario",
"password": "Contraseña",
"placeholderServerUrl": "Introducir URL del servidor",
"placeholderUsername": "Introducir usuario",
"placeholderPassword": "Introducir contraseña",
"connectedSuccess": "Conectado a PS exitosamente",
"connectedFailed": "Fallo al conectar a PS",
"syncSuccess": "Datos sincronizados a PS exitosamente",
"syncFailed": "Fallo al sincronizar datos a PS",
"sync": "Sincronizar",
"importSetting": "Ajustes de Importación",
"profile": "Perfil",
"default": "Por defecto",
"copyProfiles": "Copiar perfiles",
"addNewProfile": "Añadir nuevo perfil",
"createNewProfile": "Crear nuevo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Introducir nombre de perfil",
"profileName": "Nombre del perfil",
"create": "Crear",
"update": "Actualizar",
"characterInformation": "Información del personaje",
"skills": "Habilidades",
"showcaseCard": "Tarjeta de presentación del personaje",
"comingSoon": "Próximamente",
"characterName": "Nombre del personaje",
"placeholderCharacter": "Introducir nombre del personaje",
"characterSettings": "Ajustes del Personaje",
"levelConfiguration": "Configuración de Nivel",
"characterLevel": "Nivel de Personaje",
"max": "MÁX",
"ultimateEnergy": "Energía de Habilidad Definitiva",
"currentEnergy": "Energía al inicio del combate",
"setTo50": "Ajustar al 50%",
"battleConfiguration": "Configuración de Batalla",
"useTechnique": "Usar Técnica",
"techniqueNote": "Activar efectos de técnica pre-batalla",
"enhancement": "Mejora",
"enhancementLevel": "Nivel de Mejora",
"origin": "Original",
"enhancedNote": "Niveles superiores desbloquean más habilidades",
"lightconeEquipment": "Equipamiento de Cono de Luz",
"lightconeSettings": "Ajustes de Cono de Luz",
"placeholderLevel": "Introducir nivel",
"superimpositionRank": "Rango de Superposición",
"ranksNote": "Rangos mayores ofrecen efectos más fuertes",
"changeLightcone": "Cambiar Cono de Luz",
"removeLightcone": "Quitar Cono de Luz",
"equipLightcone": "Equipar Cono de Luz",
"noLightconeEquipped": "Sin Cono de Luz",
"equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje",
"filter": "Filtro",
"selectedCharacters": "Personajes Seleccionados",
"selectedProfiles": "Perfiles Seleccionados",
"clearAll": "Limpiar Todo",
"selectAll": "Seleccionar Todo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Ningún personaje seleccionado",
"noAvatarToCopySelected": "Ningún personaje seleccionado para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil",
"pleaseEnterUid": "Por favor, introduce UID",
"failedToFetchEnkaData": "Fallo al obtener datos de Enka",
"pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje",
"noDataToImport": "Sin datos para importar",
"pleaseSelectAFile": "Por favor, selecciona un archivo",
"fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido",
"importEnkaDataSuccess": "Datos de Enka importados con éxito",
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
"importDatabaseSuccess": "Base de datos importada con éxito",
"getData": "Obtener Datos",
"import": "Importar",
"freeSRImport": "Importar FreeSR",
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
"pickAFile": "Elegir un archivo",
"lightConeSetting": "Ajuste de Cono de Luz",
"relicMaker": "Creador de Artefactos",
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
"relicSavedSuccessfully": "Artefacto guardado exitosamente",
"mainSettings": "Ajustes Principales",
"mainStat": "Estadística Principal",
"set": "Conjunto",
"pleaseSelectASet": "Por favor, selecciona un conjunto",
"effectBonus": "Bono de Efecto",
"totalRoll": "Total de Mejoras",
"randomizeStats": "Estadísticas Aleatorias",
"randomizeRolls": "Mejoras Aleatorias",
"selectASubStat": "Seleccionar estadística secundaria",
"selectASet": "Seleccionar conjunto",
"selectAMainStat": "Seleccionar estadística principal",
"save": "Guardar",
"reset": "Reiniciar",
"roll": "Mejora",
"step": "Paso",
"memoryOfChaos": "Memoria del Caos",
"pureFiction": "Pura Ficción",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Enemigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Piso",
"side": "Lado",
"wave": "Oleada",
"stage": "Etapa",
"useCycleCount": "¿Usar conteo de ciclos?",
"useTurbulenceBuff": "¿Usar buff de turbulencia?",
"firstHalfEnemies": "Enemigos primera mitad",
"secondHalfEnemies": "Enemigos segunda mitad",
"firstNodeEnemies": "Enemigos del nodo 1",
"secondNodeEnemies": "Enemigos del nodo 2",
"thirdNodeEnemies": "Enemigos del nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de enemigos",
"turbulenceBuff": "Buff de Turbulencia",
"noEventSelected": "Ningún evento seleccionado",
"noTurbulenceBuff": "Sin Buff de Turbulencia",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Seleccionar evento de MOC",
"selectPFEvent": "Seleccionar evento de PF",
"selectASEvent": "Seleccionar evento de AS",
"selectCEEvent": "Seleccionar evento de CE",
"selectEvent": "Seleccionar Evento",
"selectFloor": "Seleccionar Piso",
"selectSide": "Seleccionar Lado",
"selectBuff": "Seleccionar Buff",
"selectStage": "Seleccionar Etapa",
"previous": "Anterior",
"next": "Siguiente",
"noMonstersFound": "No se encontraron monstruos",
"addNewWave": "Añadir Nueva Oleada",
"searchStage": "Buscar etapa...",
"noStageFound": "No se encontró etapa",
"searchMonster": "Buscar monstruo...",
"changeRelic": "Cambiar artefacto",
"deleteRelic": "Borrar artefacto",
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
"setEffects": "Configurar Efectos",
"details": "Detalles",
"normal": "Ataque Básico",
"bpskill": "Habilidad",
"maze": "Técnica",
"ultra": "Habilidad Definitiva",
"assist": "Habilidad de asistencia",
"servantskill": "Habilidad de Mnemoduende",
"severaltalent": "Talento de Mnemoduende",
"singleattack": "Ataque Individual",
"enhance": "Mejorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Ráfaga",
"restore": "Restaurar",
"support": "Soporte",
"aoeattack": "Ataque en Área",
"impair": "Debilitación",
"bounce": "Rebote",
"active": "Activo",
"defence": "Defensa",
"inactive": "Inactivo",
"maxAll": "Maximizar Todo",
"maxAllSuccess": "Habilidades maximizadas con éxito.",
"maxAllFailed": "Fallo al maximizar habilidades.",
"noRelicEquipped": "Sin artefacto equipado",
"anomalyArbitration": "Arbitraje Atípico",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Seleccionar evento PEAK",
"mode": "Modo",
"selectMode": "Seleccionar un modo",
"rollBack": "Retroceder",
"upRoll": "Subir Mejora",
"downRoll": "Bajar Mejora",
"actions": "Acciones",
"avatars": "Avatares",
"quickView": "Vista Rápida",
"extraSetting": "Ajustes Extra",
"disableCensorship": "Desactivar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Conteo de Ciclos",
"pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas",
"subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personaje Multivía",
"mainPath": "Vía Principal",
"march7Path": "Vía de Siete de Marzo",
"challenge": "Desafío",
"skipNode": "Saltar Nodo",
"disableSkip": "Desactivar salto",
"skipNode1": "Saltar nodo 1",
"skipNode2": "Saltar nodo 2",
"extraFeatures": "Características Extra",
"detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.",
"detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.",
"detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.",
"detailHiddenUi": "Oculta la interfaz del juego.",
"detailDisableCensorship": "Desactiva la censura dentro del juego.",
"detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.",
"trailblazer": "Trazacaminos",
"listExtraEffect": "Lista de Efectos Extra",
"extra": "Extra",
"customLineup": "Alineación de equipo personalizada"
}
"severaltalent": "Talento de Mnemoduende",
"singleattack": "Ataque Individual",
"enhance": "Mejorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Ráfaga",
"restore": "Restaurar",
"support": "Soporte",
"aoeattack": "Ataque en Área",
"impair": "Debilitación",
"bounce": "Rebote",
"active": "Activo",
"defence": "Defensa",
"inactive": "Inactivo",
"maxAll": "Maximizar Todo",
"maxAllSuccess": "Habilidades maximizadas con éxito.",
"maxAllFailed": "Fallo al maximizar habilidades.",
"noRelicEquipped": "Sin artefacto equipado",
"anomalyArbitration": "Arbitraje Atípico",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Seleccionar evento PEAK",
"mode": "Modo",
"selectMode": "Seleccionar un modo",
"rollBack": "Retroceder",
"upRoll": "Subir Mejora",
"downRoll": "Bajar Mejora",
"actions": "Acciones",
"avatars": "Avatares",
"quickView": "Vista Rápida",
"extraSetting": "Ajustes Extra",
"disableCensorship": "Desactivar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Conteo de Ciclos",
"pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas",
"subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personaje Multivía",
"mainPath": "Vía Principal",
"march7Path": "Vía de Siete de Marzo",
"challenge": "Desafío",
"skipNode": "Saltar Nodo",
"disableSkip": "Desactivar salto",
"skipNode1": "Saltar nodo 1",
"skipNode2": "Saltar nodo 2",
"extraFeatures": "Características Extra",
"detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.",
"detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.",
"detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.",
"detailHiddenUi": "Oculta la interfaz del juego.",
"detailDisableCensorship": "Desactiva la censura dentro del juego.",
"detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.",
"trailblazer": "Trazacaminos",
"listExtraEffect": "Lista de Efectos Extra",
"extra": "Extra",
"customLineup": "Alineación de equipo personalizada",
"def": "DEF",
"language": "Idioma"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Outils Firefly par Firefly Shelter"
},
"DataPage": {
"skillType": "Type de Compétence",
"skillName": "Nom de Compétence",
"character": "Personnage",
"id": "Id",
"path": "Voie",
"rarity": "Rareté",
"element": "Élément",
"technique": "Technique",
"talent": "Talent",
"basic": "Attaque Normale",
"skill": "Compétence",
"ultimate": "Ultime",
"servant": "Serviteur",
"damage": "Dégâts",
"type": "Type",
"warrior": "La Destruction",
"knight": "La Préservation",
"mage": "L'Érudition",
"priest": "L'Abondance",
"rogue": "La Chasse",
"shaman": "L'Harmonie",
"warlock": "La Nihilité",
"memory": "Le Souvenir",
"elation": "L'Allégresse",
"fire": "Feu",
"ice": "Glace",
"imaginary": "Imaginaire",
"physical": "Physique",
"quantum": "Quantique",
"thunder": "Foudre",
"wind": "Vent",
"hp": "PV",
"atk": "ATQ",
"speed": "VIT",
"critRate": "Taux CRIT",
"critDmg": "DGT CRIT",
"breakEffect": "Effet de Rupture",
"effectRes": "RÉS aux effets",
"energyRegenerationRate": "Taux de régénération d'énergie",
"effectHitRate": "Chances d'application d'effets",
"outgoingHealingBoost": "Augmentation des soins",
"fireDmgBoost": "Bonus de dégâts de Feu",
"iceDmgBoost": "Bonus de dégâts de Glace",
"imaginaryDmgBoost": "Bonus de dégâts Imaginaire",
"physicalDmgBoost": "Bonus de dégâts Physique",
"quantumDmgBoost": "Bonus de dégâts Quantique",
"thunderDmgBoost": "Bonus de dégâts de Foudre",
"windDmgBoost": "Bonus de dégâts de Vent",
"pursued": "Dégâts additionnels",
"true damage": "Dégâts bruts",
"elationdamage": "Dégâts d'Allégresse",
"follow-up": "Dégâts d'attaque de suivi",
"elemental damage": "Dégâts de Rupture et Super Rupture",
"dot": "Dégâts sur la durée",
"qte": "Compétence QTE",
"level": "Niveau",
"relics": "Reliques",
"eidolons": "Eidolons",
"lightcones": "Cônes de Lumière",
"loadData": "Charger les données",
"exportData": "Exporter les données",
"connectSetting": "Paramètres de connexion",
"connected": "Connecté",
"unconnected": "Déconnecté",
"psConnection": "Connexion PS",
"connectionType": "Type de connexion",
"status": "Statut",
"connectPs": "Connecter PS",
"disconnect": "Déconnecter",
"other": "Autre",
"freeSr": "FreeSR",
"database": "Base de données",
"enka": "Enka",
"monsterSetting": "Paramètre de monstres",
"serverUrl": "URL du Serveur",
"privateType": "Type privé",
"local": "Local",
"server": "Serveur",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"placeholderServerUrl": "Entrer l'URL du serveur",
"placeholderUsername": "Entrer le nom d'utilisateur",
"placeholderPassword": "Entrer le mot de passe",
"connectedSuccess": "Connecté au PS avec succès",
"connectedFailed": "Échec de connexion au PS",
"syncSuccess": "Données synchronisées au PS avec succès",
"syncFailed": "Échec de synchronisation au PS",
"sync": "Synchroniser",
"importSetting": "Paramètres d'importation",
"profile": "Profil",
"default": "Défaut",
"copyProfiles": "Copier les profils",
"addNewProfile": "Ajouter un nouveau profil",
"createNewProfile": "Créer un nouveau profil",
"editProfile": "Éditer le profil",
"placeholderProfileName": "Entrer le nom du profil",
"profileName": "Nom du profil",
"create": "Créer",
"update": "Mettre à jour",
"characterInformation": "Informations du personnage",
"skills": "Compétences",
"showcaseCard": "Carte de présentation",
"comingSoon": "Bientôt disponible",
"characterName": "Nom du personnage",
"placeholderCharacter": "Entrer le nom du personnage",
"characterSettings": "Paramètres du personnage",
"levelConfiguration": "Configuration de niveau",
"characterLevel": "Niveau du personnage",
"max": "MAX",
"ultimateEnergy": "Énergie de l'Ultime",
"currentEnergy": "Énergie actuelle",
"setTo50": "Régler sur 50%",
"battleConfiguration": "Configuration de combat",
"useTechnique": "Utiliser Technique",
"techniqueNote": "Activer les effets de technique avant le combat",
"enhancement": "Amélioration",
"enhancementLevel": "Niveau d'amélioration",
"origin": "Origine",
"enhancedNote": "Les améliorations élevées débloquent des compétences",
"lightconeEquipment": "Équipement de Cône de Lumière",
"lightconeSettings": "Paramètres de Cône de Lumière",
"placeholderLevel": "Entrer le niveau",
"superimpositionRank": "Rang de Superposition",
"ranksNote": "Les rangs élevés offrent des effets plus forts",
"changeLightcone": "Changer de Cône de Lumière",
"removeLightcone": "Retirer le Cône de Lumière",
"equipLightcone": "Équiper un Cône de Lumière",
"noLightconeEquipped": "Aucun Cône de Lumière",
"equipLightconeNote": "Équipez un cône pour renforcer votre personnage",
"filter": "Filtre",
"selectedCharacters": "Personnages sélectionnés",
"selectedProfiles": "Profils sélectionnés",
"clearAll": "Tout effacer",
"selectAll": "Tout sélectionner",
"copy": "Copier",
"copied": "Copié",
"noAvatarSelected": "Aucun personnage sélectionné",
"noAvatarToCopySelected": "Aucun personnage à copier sélectionné",
"pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil",
"pleaseEnterUid": "Veuillez entrer un UID",
"failedToFetchEnkaData": "Échec de récupération des données Enka",
"pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage",
"noDataToImport": "Aucune donnée à importer",
"pleaseSelectAFile": "Veuillez sélectionner un fichier",
"fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide",
"importEnkaDataSuccess": "Importation Enka réussie",
"importFreeSRDataSuccess": "Importation FreeSR réussie",
"importDatabaseSuccess": "Importation de base de données réussie",
"getData": "Obtenir des données",
"import": "Importer",
"freeSRImport": "Importation FreeSR",
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
"pickAFile": "Choisir un fichier",
"lightConeSetting": "Paramètre de Cône de Lumière",
"relicMaker": "Créateur de Reliques",
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
"relicSavedSuccessfully": "Relique sauvegardée avec succès",
"mainSettings": "Paramètres principaux",
"mainStat": "Statistique principale",
"set": "Set",
"pleaseSelectASet": "Veuillez sélectionner un set",
"effectBonus": "Bonus d'effet",
"totalRoll": "Total d'améliorations",
"randomizeStats": "Statistiques aléatoires",
"randomizeRolls": "Améliorations aléatoires",
"selectASubStat": "Sélectionner une stat secondaire",
"selectASet": "Sélectionner un set",
"selectAMainStat": "Sélectionner une stat principale",
"save": "Sauvegarder",
"reset": "Réinitialiser",
"roll": "Amélioration",
"step": "Étape",
"memoryOfChaos": "Mémoire du Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Ombre Apocalyptique",
"customEnemy": "Ennemi personnalisé",
"simulatedUniverse": "Univers Simulé",
"floor": "Étage",
"side": "Côté",
"wave": "Vague",
"stage": "Niveau",
"useCycleCount": "Utiliser le compteur de cycles ?",
"useTurbulenceBuff": "Utiliser le buff de turbulence ?",
"firstHalfEnemies": "Ennemis première moitié",
"secondHalfEnemies": "Ennemis deuxième moitié",
"firstNodeEnemies": "Ennemis du nœud 1",
"secondNodeEnemies": "Ennemis du nœud 2",
"thirdNodeEnemies": "Ennemis du nœud 3",
"firstNode": "Nœud 1",
"secondNode": "Nœud 2",
"thirdNode": "Nœud 3",
"listEnemies": "Liste des ennemis",
"turbulenceBuff": "Buff de Turbulence",
"noEventSelected": "Aucun événement sélectionné",
"noTurbulenceBuff": "Aucun Buff de Turbulence",
"upper": "Supérieur",
"lower": "Inférieur",
"upperToLower": "Supérieur -> Inférieur",
"lowerToUpper": "Inférieur -> Supérieur",
"selectMOCEvent": "Sélectionner un événement MOC",
"selectPFEvent": "Sélectionner un événement PF",
"selectASEvent": "Sélectionner un événement AS",
"selectCEEvent": "Sélectionner un événement CE",
"selectEvent": "Sélectionner un événement",
"selectFloor": "Sélectionner un étage",
"selectSide": "Sélectionner un côté",
"selectBuff": "Sélectionner un buff",
"selectStage": "Sélectionner un niveau",
"previous": "Précédent",
"next": "Suivant",
"noMonstersFound": "Aucun monstre trouvé",
"addNewWave": "Ajouter une nouvelle vague",
"searchStage": "Chercher un niveau...",
"noStageFound": "Aucun niveau trouvé",
"searchMonster": "Chercher un monstre...",
"changeRelic": "Changer de relique",
"deleteRelic": "Supprimer la relique",
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
"setEffects": "Définir les effets",
"details": "Détails",
"normal": "Attaque Normale",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Outils Firefly par Firefly Shelter"
},
"DataPage": {
"skillType": "Type de Compétence",
"skillName": "Nom de Compétence",
"character": "Personnage",
"id": "Id",
"path": "Voie",
"rarity": "Rareté",
"element": "Élément",
"technique": "Technique",
"talent": "Talent",
"basic": "Attaque Normale",
"skill": "Compétence",
"ultimate": "Ultime",
"servant": "Serviteur",
"damage": "Dégâts",
"type": "Type",
"warrior": "La Destruction",
"knight": "La Préservation",
"mage": "L'Érudition",
"priest": "L'Abondance",
"rogue": "La Chasse",
"shaman": "L'Harmonie",
"warlock": "La Nihilité",
"memory": "Le Souvenir",
"elation": "L'Allégresse",
"fire": "Feu",
"ice": "Glace",
"imaginary": "Imaginaire",
"physical": "Physique",
"quantum": "Quantique",
"thunder": "Foudre",
"wind": "Vent",
"hp": "PV",
"atk": "ATQ",
"speed": "VIT",
"critRate": "Taux CRIT",
"critDmg": "DGT CRIT",
"breakEffect": "Effet de Rupture",
"effectRes": "RÉS aux effets",
"energyRegenerationRate": "Taux de régénération d'énergie",
"effectHitRate": "Chances d'application d'effets",
"outgoingHealingBoost": "Augmentation des soins",
"fireDmgBoost": "Bonus de dégâts de Feu",
"iceDmgBoost": "Bonus de dégâts de Glace",
"imaginaryDmgBoost": "Bonus de dégâts Imaginaire",
"physicalDmgBoost": "Bonus de dégâts Physique",
"quantumDmgBoost": "Bonus de dégâts Quantique",
"thunderDmgBoost": "Bonus de dégâts de Foudre",
"windDmgBoost": "Bonus de dégâts de Vent",
"pursued": "Dégâts additionnels",
"true damage": "Dégâts bruts",
"elationdamage": "Dégâts d'Allégresse",
"follow-up": "Dégâts d'attaque de suivi",
"elemental damage": "Dégâts de Rupture et Super Rupture",
"dot": "Dégâts sur la durée",
"qte": "Compétence QTE",
"level": "Niveau",
"relics": "Reliques",
"eidolons": "Eidolons",
"lightcones": "Cônes de Lumière",
"loadData": "Charger les données",
"exportData": "Exporter les données",
"connectSetting": "Paramètres de connexion",
"connected": "Connecté",
"unconnected": "Déconnecté",
"psConnection": "Connexion PS",
"connectionType": "Type de connexion",
"status": "Statut",
"connectPs": "Connecter PS",
"disconnect": "Déconnecter",
"other": "Autre",
"freeSr": "FreeSR",
"database": "Base de données",
"enka": "Enka",
"monsterSetting": "Paramètre de monstres",
"serverUrl": "URL du Serveur",
"privateType": "Type privé",
"local": "Local",
"server": "Serveur",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"placeholderServerUrl": "Entrer l'URL du serveur",
"placeholderUsername": "Entrer le nom d'utilisateur",
"placeholderPassword": "Entrer le mot de passe",
"connectedSuccess": "Connecté au PS avec succès",
"connectedFailed": "Échec de connexion au PS",
"syncSuccess": "Données synchronisées au PS avec succès",
"syncFailed": "Échec de synchronisation au PS",
"sync": "Synchroniser",
"importSetting": "Paramètres d'importation",
"profile": "Profil",
"default": "Défaut",
"copyProfiles": "Copier les profils",
"addNewProfile": "Ajouter un nouveau profil",
"createNewProfile": "Créer un nouveau profil",
"editProfile": "Éditer le profil",
"placeholderProfileName": "Entrer le nom du profil",
"profileName": "Nom du profil",
"create": "Créer",
"update": "Mettre à jour",
"characterInformation": "Informations du personnage",
"skills": "Compétences",
"showcaseCard": "Carte de présentation",
"comingSoon": "Bientôt disponible",
"characterName": "Nom du personnage",
"placeholderCharacter": "Entrer le nom du personnage",
"characterSettings": "Paramètres du personnage",
"levelConfiguration": "Configuration de niveau",
"characterLevel": "Niveau du personnage",
"max": "MAX",
"ultimateEnergy": "Énergie de l'Ultime",
"currentEnergy": "Énergie actuelle",
"setTo50": "Régler sur 50%",
"battleConfiguration": "Configuration de combat",
"useTechnique": "Utiliser Technique",
"techniqueNote": "Activer les effets de technique avant le combat",
"enhancement": "Amélioration",
"enhancementLevel": "Niveau d'amélioration",
"origin": "Origine",
"enhancedNote": "Les améliorations élevées débloquent des compétences",
"lightconeEquipment": "Équipement de Cône de Lumière",
"lightconeSettings": "Paramètres de Cône de Lumière",
"placeholderLevel": "Entrer le niveau",
"superimpositionRank": "Rang de Superposition",
"ranksNote": "Les rangs élevés offrent des effets plus forts",
"changeLightcone": "Changer de Cône de Lumière",
"removeLightcone": "Retirer le Cône de Lumière",
"equipLightcone": "Équiper un Cône de Lumière",
"noLightconeEquipped": "Aucun Cône de Lumière",
"equipLightconeNote": "Équipez un cône pour renforcer votre personnage",
"filter": "Filtre",
"selectedCharacters": "Personnages sélectionnés",
"selectedProfiles": "Profils sélectionnés",
"clearAll": "Tout effacer",
"selectAll": "Tout sélectionner",
"copy": "Copier",
"copied": "Copié",
"noAvatarSelected": "Aucun personnage sélectionné",
"noAvatarToCopySelected": "Aucun personnage à copier sélectionné",
"pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil",
"pleaseEnterUid": "Veuillez entrer un UID",
"failedToFetchEnkaData": "Échec de récupération des données Enka",
"pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage",
"noDataToImport": "Aucune donnée à importer",
"pleaseSelectAFile": "Veuillez sélectionner un fichier",
"fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide",
"importEnkaDataSuccess": "Importation Enka réussie",
"importFreeSRDataSuccess": "Importation FreeSR réussie",
"importDatabaseSuccess": "Importation de base de données réussie",
"getData": "Obtenir des données",
"import": "Importer",
"freeSRImport": "Importation FreeSR",
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
"pickAFile": "Choisir un fichier",
"lightConeSetting": "Paramètre de Cône de Lumière",
"relicMaker": "Créateur de Reliques",
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
"relicSavedSuccessfully": "Relique sauvegardée avec succès",
"mainSettings": "Paramètres principaux",
"mainStat": "Statistique principale",
"set": "Set",
"pleaseSelectASet": "Veuillez sélectionner un set",
"effectBonus": "Bonus d'effet",
"totalRoll": "Total d'améliorations",
"randomizeStats": "Statistiques aléatoires",
"randomizeRolls": "Améliorations aléatoires",
"selectASubStat": "Sélectionner une stat secondaire",
"selectASet": "Sélectionner un set",
"selectAMainStat": "Sélectionner une stat principale",
"save": "Sauvegarder",
"reset": "Réinitialiser",
"roll": "Amélioration",
"step": "Étape",
"memoryOfChaos": "Mémoire du Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Ombre Apocalyptique",
"customEnemy": "Ennemi personnalisé",
"simulatedUniverse": "Univers Simulé",
"floor": "Étage",
"side": "Côté",
"wave": "Vague",
"stage": "Niveau",
"useCycleCount": "Utiliser le compteur de cycles ?",
"useTurbulenceBuff": "Utiliser le buff de turbulence ?",
"firstHalfEnemies": "Ennemis première moitié",
"secondHalfEnemies": "Ennemis deuxième moitié",
"firstNodeEnemies": "Ennemis du nœud 1",
"secondNodeEnemies": "Ennemis du nœud 2",
"thirdNodeEnemies": "Ennemis du nœud 3",
"firstNode": "Nœud 1",
"secondNode": "Nœud 2",
"thirdNode": "Nœud 3",
"listEnemies": "Liste des ennemis",
"turbulenceBuff": "Buff de Turbulence",
"noEventSelected": "Aucun événement sélectionné",
"noTurbulenceBuff": "Aucun Buff de Turbulence",
"upper": "Supérieur",
"lower": "Inférieur",
"upperToLower": "Supérieur -> Inférieur",
"lowerToUpper": "Inférieur -> Supérieur",
"selectMOCEvent": "Sélectionner un événement MOC",
"selectPFEvent": "Sélectionner un événement PF",
"selectASEvent": "Sélectionner un événement AS",
"selectCEEvent": "Sélectionner un événement CE",
"selectEvent": "Sélectionner un événement",
"selectFloor": "Sélectionner un étage",
"selectSide": "Sélectionner un côté",
"selectBuff": "Sélectionner un buff",
"selectStage": "Sélectionner un niveau",
"previous": "Précédent",
"next": "Suivant",
"noMonstersFound": "Aucun monstre trouvé",
"addNewWave": "Ajouter une nouvelle vague",
"searchStage": "Chercher un niveau...",
"noStageFound": "Aucun niveau trouvé",
"searchMonster": "Chercher un monstre...",
"changeRelic": "Changer de relique",
"deleteRelic": "Supprimer la relique",
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
"setEffects": "Définir les effets",
"details": "Détails",
"normal": "Attaque Normale",
"bpskill": "Compétence",
"maze": "Technique",
"ultra": "Ultime",
"assist": "Compétence d'assistance",
"servantskill": "Compétence de Mémolutin",
"severaltalent": "Talent de Mémolutin",
"singleattack": "Attaque à cible unique",
"enhance": "Renforcement",
"summon": "Invocation",
"mazeattack": "Attaque de Technique",
"blast": "Diffusion",
"restore": "Restauration",
"support": "Soutien",
"aoeattack": "Attaque de zone",
"impair": "Malus",
"bounce": "Rebond",
"active": "Actif",
"defence": "Défense",
"inactive": "Inactif",
"maxAll": "Tout maximiser",
"maxAllSuccess": "Compétences maximisées avec succès.",
"maxAllFailed": "Échec de maximisation.",
"noRelicEquipped": "Aucune relique équipée",
"anomalyArbitration": "Arbitrage d'Anomalie",
"normalMode": "Mode Normal",
"hardMode": "Mode Difficile",
"selectPEAKEvent": "Sélectionner un événement PEAK",
"mode": "Mode",
"selectMode": "Sélectionner un mode",
"rollBack": "Annuler l'étape",
"upRoll": "Augmenter l'amélioration",
"downRoll": "Diminuer l'amélioration",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Aperçu rapide",
"extraSetting": "Paramètres supplémentaires",
"disableCensorship": "Désactiver la censure",
"hideUI": "Cacher l'UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Nombre de cycles",
"pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires",
"subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personnage Multi-Voies",
"mainPath": "Voie Principale",
"march7Path": "Voie de March 7th",
"challenge": "Défi",
"skipNode": "Passer le nœud",
"disableSkip": "Désactiver le passage",
"skipNode1": "Passer le nœud 1",
"skipNode2": "Passer le nœud 2",
"extraFeatures": "Fonctionnalités supplémentaires",
"detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.",
"detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.",
"detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.",
"detailHiddenUi": "Cachera l'interface du jeu.",
"detailDisableCensorship": "Désactivera la censure du jeu.",
"detailMultipathCharacter": "Permet de changer la Voie de certains personnages.",
"trailblazer": "Pionnier",
"listExtraEffect": "Liste des effets supplémentaires",
"extra": "Extra",
"customLineup": "Composition personnalisée"
}
"severaltalent": "Talent de Mémolutin",
"singleattack": "Attaque à cible unique",
"enhance": "Renforcement",
"summon": "Invocation",
"mazeattack": "Attaque de Technique",
"blast": "Diffusion",
"restore": "Restauration",
"support": "Soutien",
"aoeattack": "Attaque de zone",
"impair": "Malus",
"bounce": "Rebond",
"active": "Actif",
"defence": "Défense",
"inactive": "Inactif",
"maxAll": "Tout maximiser",
"maxAllSuccess": "Compétences maximisées avec succès.",
"maxAllFailed": "Échec de maximisation.",
"noRelicEquipped": "Aucune relique équipée",
"anomalyArbitration": "Arbitrage d'Anomalie",
"normalMode": "Mode Normal",
"hardMode": "Mode Difficile",
"selectPEAKEvent": "Sélectionner un événement PEAK",
"mode": "Mode",
"selectMode": "Sélectionner un mode",
"rollBack": "Annuler l'étape",
"upRoll": "Augmenter l'amélioration",
"downRoll": "Diminuer l'amélioration",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Aperçu rapide",
"extraSetting": "Paramètres supplémentaires",
"disableCensorship": "Désactiver la censure",
"hideUI": "Cacher l'UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Nombre de cycles",
"pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires",
"subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personnage Multi-Voies",
"mainPath": "Voie Principale",
"march7Path": "Voie de March 7th",
"challenge": "Défi",
"skipNode": "Passer le nœud",
"disableSkip": "Désactiver le passage",
"skipNode1": "Passer le nœud 1",
"skipNode2": "Passer le nœud 2",
"extraFeatures": "Fonctionnalités supplémentaires",
"detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.",
"detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.",
"detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.",
"detailHiddenUi": "Cachera l'interface du jeu.",
"detailDisableCensorship": "Désactivera la censure du jeu.",
"detailMultipathCharacter": "Permet de changer la Voie de certains personnages.",
"trailblazer": "Pionnier",
"listExtraEffect": "Liste des effets supplémentaires",
"extra": "Extra",
"customLineup": "Composition personnalisée",
"def": "DÉF",
"language": "Langue"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Alat Firefly oleh Firefly Shelter"
},
"DataPage": {
"skillType": "Tipe Skill",
"skillName": "Nama Skill",
"character": "Karakter",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Elemen",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Tipe",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Lightning",
"wind": "Wind",
"hp": "HP",
"atk": "ATK",
"speed": "SPD",
"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 DMG Boost",
"iceDmgBoost": "Ice DMG Boost",
"imaginaryDmgBoost": "Imaginary DMG Boost",
"physicalDmgBoost": "Physical DMG Boost",
"quantumDmgBoost": "Quantum DMG Boost",
"thunderDmgBoost": "Lightning DMG Boost",
"windDmgBoost": "Wind DMG Boost",
"pursued": "Additional Damage",
"true damage": "True Damage",
"elationdamage": "Elation Damage",
"follow-up": "Follow-up Attack Damage",
"elemental damage": "Break dan Super Break Damage",
"dot": "Damage Over Time",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relic",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "Muat data",
"exportData": "Ekspor data",
"connectSetting": "Pengaturan Koneksi",
"connected": "Terhubung",
"unconnected": "Tidak terhubung",
"psConnection": "Koneksi PS",
"connectionType": "Tipe Koneksi",
"status": "Status",
"connectPs": "Hubungkan PS",
"disconnect": "Putuskan sambungan",
"other": "Lainnya",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Pengaturan Monster",
"serverUrl": "URL Server",
"privateType": "Tipe Private",
"local": "Lokal",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Masukkan URL server",
"placeholderUsername": "Masukkan username",
"placeholderPassword": "Masukkan password",
"connectedSuccess": "Berhasil terhubung ke PS",
"connectedFailed": "Gagal terhubung ke PS",
"syncSuccess": "Berhasil sinkronisasi data ke PS",
"syncFailed": "Gagal sinkronisasi data ke PS",
"sync": "Sinkronisasi",
"importSetting": "Pengaturan Impor",
"profile": "Profil",
"default": "Default",
"copyProfiles": "Salin profil",
"addNewProfile": "Tambah profil baru",
"createNewProfile": "Buat profil baru",
"editProfile": "Edit profil",
"placeholderProfileName": "Masukkan nama profil",
"profileName": "Nama profil",
"create": "Buat",
"update": "Perbarui",
"characterInformation": "Informasi Karakter",
"skills": "Skill",
"showcaseCard": "Showcase Card",
"comingSoon": "Segera hadir",
"characterName": "Nama Karakter",
"placeholderCharacter": "Masukkan nama karakter",
"characterSettings": "Pengaturan Karakter",
"levelConfiguration": "Konfigurasi Level",
"characterLevel": "Level Karakter",
"max": "MAX",
"ultimateEnergy": "Energy Ultimate",
"currentEnergy": "Energy Saat Ini",
"setTo50": "Atur ke 50%",
"battleConfiguration": "Konfigurasi Pertempuran",
"useTechnique": "Gunakan Technique",
"techniqueNote": "Aktifkan efek technique sebelum pertempuran",
"enhancement": "Enhancement",
"enhancementLevel": "Level Enhancement",
"origin": "Asli",
"enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan",
"lightconeEquipment": "Peralatan Light Cone",
"lightconeSettings": "Pengaturan Light Cone",
"placeholderLevel": "Masukkan level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Rank lebih tinggi memberikan efek lebih kuat",
"changeLightcone": "Ganti Light Cone",
"removeLightcone": "Lepas Light Cone",
"equipLightcone": "Pakai Light Cone",
"noLightconeEquipped": "Tidak Ada Light Cone",
"equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu",
"filter": "Filter",
"selectedCharacters": "Karakter Dipilih",
"selectedProfiles": "Profil Dipilih",
"clearAll": "Bersihkan Semua",
"selectAll": "Pilih Semua",
"copy": "Salin",
"copied": "Tersalin",
"noAvatarSelected": "Tidak ada karakter dipilih",
"noAvatarToCopySelected": "Tidak ada karakter untuk disalin",
"pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil",
"pleaseEnterUid": "Silakan masukkan UID",
"failedToFetchEnkaData": "Gagal mengambil data Enka",
"pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter",
"noDataToImport": "Tidak ada data untuk diimpor",
"pleaseSelectAFile": "Silakan pilih file",
"fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid",
"importEnkaDataSuccess": "Impor data Enka berhasil",
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
"importDatabaseSuccess": "Impor database berhasil",
"getData": "Dapatkan Data",
"import": "Impor",
"freeSRImport": "Impor FreeSR",
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
"pickAFile": "Pilih sebuah file",
"lightConeSetting": "Pengaturan Light Cone",
"relicMaker": "Pembuat Relic",
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
"relicSavedSuccessfully": "Relic berhasil disimpan",
"mainSettings": "Pengaturan Utama",
"mainStat": "Statistik Utama",
"set": "Set",
"pleaseSelectASet": "Pilih sebuah set",
"effectBonus": "Bonus Efek",
"totalRoll": "Total Roll",
"randomizeStats": "Acak Statistik",
"randomizeRolls": "Acak Roll",
"selectASubStat": "Pilih sub stat",
"selectASet": "Pilih sebuah set",
"selectAMainStat": "Pilih stat utama",
"save": "Simpan",
"reset": "Reset",
"roll": "Roll",
"step": "Langkah",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Musuh Kustom",
"simulatedUniverse": "Simulated Universe",
"floor": "Lantai",
"side": "Sisi",
"wave": "Gelombang",
"stage": "Tahap",
"useCycleCount": "Gunakan hitungan siklus?",
"useTurbulenceBuff": "Gunakan buff turbulence?",
"firstHalfEnemies": "Musuh paruh pertama",
"secondHalfEnemies": "Musuh paruh kedua",
"firstNodeEnemies": "Musuh Node 1",
"secondNodeEnemies": "Musuh Node 2",
"thirdNodeEnemies": "Musuh Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"listEnemies": "Daftar musuh",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "Tidak ada event dipilih",
"noTurbulenceBuff": "Tanpa Turbulence Buff",
"upper": "Atas",
"lower": "Bawah",
"upperToLower": "Atas -> Bawah",
"lowerToUpper": "Bawah -> Atas",
"selectMOCEvent": "Pilih Event MOC",
"selectPFEvent": "Pilih Event PF",
"selectASEvent": "Pilih Event AS",
"selectCEEvent": "Pilih Event CE",
"selectEvent": "Pilih Event",
"selectFloor": "Pilih Lantai",
"selectSide": "Pilih Sisi",
"selectBuff": "Pilih Buff",
"selectStage": "Pilih Tahap",
"previous": "Sebelumnya",
"next": "Selanjutnya",
"noMonstersFound": "Tidak ada monster ditemukan",
"addNewWave": "Tambah Gelombang Baru",
"searchStage": "Cari tahap...",
"noStageFound": "Tahap tidak ditemukan",
"searchMonster": "Cari monster...",
"changeRelic": "Ganti relic",
"deleteRelic": "Hapus relic",
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
"setEffects": "Atur Efek",
"details": "Detail",
"normal": "Basic ATK",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Alat Firefly oleh Firefly Shelter"
},
"DataPage": {
"skillType": "Tipe Skill",
"skillName": "Nama Skill",
"character": "Karakter",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Elemen",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Tipe",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Lightning",
"wind": "Wind",
"hp": "HP",
"atk": "ATK",
"speed": "SPD",
"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 DMG Boost",
"iceDmgBoost": "Ice DMG Boost",
"imaginaryDmgBoost": "Imaginary DMG Boost",
"physicalDmgBoost": "Physical DMG Boost",
"quantumDmgBoost": "Quantum DMG Boost",
"thunderDmgBoost": "Lightning DMG Boost",
"windDmgBoost": "Wind DMG Boost",
"pursued": "Additional Damage",
"true damage": "True Damage",
"elationdamage": "Elation Damage",
"follow-up": "Follow-up Attack Damage",
"elemental damage": "Break dan Super Break Damage",
"dot": "Damage Over Time",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relic",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "Muat data",
"exportData": "Ekspor data",
"connectSetting": "Pengaturan Koneksi",
"connected": "Terhubung",
"unconnected": "Tidak terhubung",
"psConnection": "Koneksi PS",
"connectionType": "Tipe Koneksi",
"status": "Status",
"connectPs": "Hubungkan PS",
"disconnect": "Putuskan sambungan",
"other": "Lainnya",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Pengaturan Monster",
"serverUrl": "URL Server",
"privateType": "Tipe Private",
"local": "Lokal",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Masukkan URL server",
"placeholderUsername": "Masukkan username",
"placeholderPassword": "Masukkan password",
"connectedSuccess": "Berhasil terhubung ke PS",
"connectedFailed": "Gagal terhubung ke PS",
"syncSuccess": "Berhasil sinkronisasi data ke PS",
"syncFailed": "Gagal sinkronisasi data ke PS",
"sync": "Sinkronisasi",
"importSetting": "Pengaturan Impor",
"profile": "Profil",
"default": "Default",
"copyProfiles": "Salin profil",
"addNewProfile": "Tambah profil baru",
"createNewProfile": "Buat profil baru",
"editProfile": "Edit profil",
"placeholderProfileName": "Masukkan nama profil",
"profileName": "Nama profil",
"create": "Buat",
"update": "Perbarui",
"characterInformation": "Informasi Karakter",
"skills": "Skill",
"showcaseCard": "Showcase Card",
"comingSoon": "Segera hadir",
"characterName": "Nama Karakter",
"placeholderCharacter": "Masukkan nama karakter",
"characterSettings": "Pengaturan Karakter",
"levelConfiguration": "Konfigurasi Level",
"characterLevel": "Level Karakter",
"max": "MAX",
"ultimateEnergy": "Energy Ultimate",
"currentEnergy": "Energy Saat Ini",
"setTo50": "Atur ke 50%",
"battleConfiguration": "Konfigurasi Pertempuran",
"useTechnique": "Gunakan Technique",
"techniqueNote": "Aktifkan efek technique sebelum pertempuran",
"enhancement": "Enhancement",
"enhancementLevel": "Level Enhancement",
"origin": "Asli",
"enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan",
"lightconeEquipment": "Peralatan Light Cone",
"lightconeSettings": "Pengaturan Light Cone",
"placeholderLevel": "Masukkan level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Rank lebih tinggi memberikan efek lebih kuat",
"changeLightcone": "Ganti Light Cone",
"removeLightcone": "Lepas Light Cone",
"equipLightcone": "Pakai Light Cone",
"noLightconeEquipped": "Tidak Ada Light Cone",
"equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu",
"filter": "Filter",
"selectedCharacters": "Karakter Dipilih",
"selectedProfiles": "Profil Dipilih",
"clearAll": "Bersihkan Semua",
"selectAll": "Pilih Semua",
"copy": "Salin",
"copied": "Tersalin",
"noAvatarSelected": "Tidak ada karakter dipilih",
"noAvatarToCopySelected": "Tidak ada karakter untuk disalin",
"pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil",
"pleaseEnterUid": "Silakan masukkan UID",
"failedToFetchEnkaData": "Gagal mengambil data Enka",
"pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter",
"noDataToImport": "Tidak ada data untuk diimpor",
"pleaseSelectAFile": "Silakan pilih file",
"fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid",
"importEnkaDataSuccess": "Impor data Enka berhasil",
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
"importDatabaseSuccess": "Impor database berhasil",
"getData": "Dapatkan Data",
"import": "Impor",
"freeSRImport": "Impor FreeSR",
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
"pickAFile": "Pilih sebuah file",
"lightConeSetting": "Pengaturan Light Cone",
"relicMaker": "Pembuat Relic",
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
"relicSavedSuccessfully": "Relic berhasil disimpan",
"mainSettings": "Pengaturan Utama",
"mainStat": "Statistik Utama",
"set": "Set",
"pleaseSelectASet": "Pilih sebuah set",
"effectBonus": "Bonus Efek",
"totalRoll": "Total Roll",
"randomizeStats": "Acak Statistik",
"randomizeRolls": "Acak Roll",
"selectASubStat": "Pilih sub stat",
"selectASet": "Pilih sebuah set",
"selectAMainStat": "Pilih stat utama",
"save": "Simpan",
"reset": "Reset",
"roll": "Roll",
"step": "Langkah",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Musuh Kustom",
"simulatedUniverse": "Simulated Universe",
"floor": "Lantai",
"side": "Sisi",
"wave": "Gelombang",
"stage": "Tahap",
"useCycleCount": "Gunakan hitungan siklus?",
"useTurbulenceBuff": "Gunakan buff turbulence?",
"firstHalfEnemies": "Musuh paruh pertama",
"secondHalfEnemies": "Musuh paruh kedua",
"firstNodeEnemies": "Musuh Node 1",
"secondNodeEnemies": "Musuh Node 2",
"thirdNodeEnemies": "Musuh Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"listEnemies": "Daftar musuh",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "Tidak ada event dipilih",
"noTurbulenceBuff": "Tanpa Turbulence Buff",
"upper": "Atas",
"lower": "Bawah",
"upperToLower": "Atas -> Bawah",
"lowerToUpper": "Bawah -> Atas",
"selectMOCEvent": "Pilih Event MOC",
"selectPFEvent": "Pilih Event PF",
"selectASEvent": "Pilih Event AS",
"selectCEEvent": "Pilih Event CE",
"selectEvent": "Pilih Event",
"selectFloor": "Pilih Lantai",
"selectSide": "Pilih Sisi",
"selectBuff": "Pilih Buff",
"selectStage": "Pilih Tahap",
"previous": "Sebelumnya",
"next": "Selanjutnya",
"noMonstersFound": "Tidak ada monster ditemukan",
"addNewWave": "Tambah Gelombang Baru",
"searchStage": "Cari tahap...",
"noStageFound": "Tahap tidak ditemukan",
"searchMonster": "Cari monster...",
"changeRelic": "Ganti relic",
"deleteRelic": "Hapus relic",
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
"setEffects": "Atur Efek",
"details": "Detail",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"assist": "Skill Bantuan",
"servantskill": "Skill Memosprite",
"severaltalent": "Talent Memosprite",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Aktif",
"defence": "Defence",
"inactive": "Nonaktif",
"maxAll": "Maksimalkan Semua",
"maxAllSuccess": "Berhasil memaksimalkan level skill.",
"maxAllFailed": "Gagal memaksimalkan level skill.",
"noRelicEquipped": "Tidak ada relic dipakai",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Mode Normal",
"hardMode": "Mode Sulit",
"selectPEAKEvent": "Pilih Event PEAK",
"mode": "Mode",
"selectMode": "Pilih Mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Aksi",
"avatars": "Avatar",
"quickView": "Tampilan Cepat",
"extraSetting": "Pengaturan Ekstra",
"disableCensorship": "Nonaktifkan Sensor",
"hideUI": "Sembunyikan UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Hitungan Siklus",
"pleaseSelectAllSubStats": "Pilih semua sub statistik",
"subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Karakter Multipath",
"mainPath": "Path Utama",
"march7Path": "Path March 7th",
"challenge": "Tantangan",
"skipNode": "Lewati Node",
"disableSkip": "Nonaktifkan lewati",
"skipNode1": "Lewati node 1",
"skipNode2": "Lewati node 2",
"extraFeatures": "Fitur Ekstra",
"detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.",
"detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.",
"detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.",
"detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.",
"detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.",
"detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Daftar Efek Ekstra",
"extra": "Ekstra",
"customLineup": "Lineup Kustom"
}
"severaltalent": "Talent Memosprite",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Aktif",
"defence": "Defence",
"inactive": "Nonaktif",
"maxAll": "Maksimalkan Semua",
"maxAllSuccess": "Berhasil memaksimalkan level skill.",
"maxAllFailed": "Gagal memaksimalkan level skill.",
"noRelicEquipped": "Tidak ada relic dipakai",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Mode Normal",
"hardMode": "Mode Sulit",
"selectPEAKEvent": "Pilih Event PEAK",
"mode": "Mode",
"selectMode": "Pilih Mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Aksi",
"avatars": "Avatar",
"quickView": "Tampilan Cepat",
"extraSetting": "Pengaturan Ekstra",
"disableCensorship": "Nonaktifkan Sensor",
"hideUI": "Sembunyikan UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Hitungan Siklus",
"pleaseSelectAllSubStats": "Pilih semua sub statistik",
"subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Karakter Multipath",
"mainPath": "Path Utama",
"march7Path": "Path March 7th",
"challenge": "Tantangan",
"skipNode": "Lewati Node",
"disableSkip": "Nonaktifkan lewati",
"skipNode1": "Lewati node 1",
"skipNode2": "Lewati node 2",
"extraFeatures": "Fitur Ekstra",
"detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.",
"detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.",
"detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.",
"detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.",
"detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.",
"detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Daftar Efek Ekstra",
"extra": "Ekstra",
"customLineup": "Lineup Kustom",
"def": "DEF",
"language": "Bahasa"
}
}
+292 -290
View File
@@ -1,290 +1,292 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "スキルタイプ",
"skillName": "スキル名",
"character": "キャラクター",
"id": "ID",
"path": "運命",
"rarity": "レアリティ",
"element": "属性",
"technique": "秘技",
"talent": "天賦",
"basic": "通常攻撃",
"skill": "スキル",
"ultimate": "アルティメット",
"servant": "サーバント",
"damage": "ダメージ",
"type": "タイプ",
"warrior": "壊滅",
"knight": "存護",
"mage": "知恵",
"priest": "豊穣",
"rogue": "巡狩",
"shaman": "調和",
"warlock": "虚無",
"memory": "記憶",
"elation": "愉悦",
"fire": "火",
"ice": "氷",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
"wind": "風",
"hp": "HP",
"atk": "攻撃",
"speed": "速度",
"critRate": "会心率",
"critDmg": "会心ダメージ",
"breakEffect": "撃破特効",
"effectRes": "効果抵抗",
"energyRegenerationRate": "EP回復効率",
"effectHitRate": "効果命中率",
"outgoingHealingBoost": "回復増加",
"fireDmgBoost": "火属性ダメージ強化",
"iceDmgBoost": "氷属性ダメージ強化",
"imaginaryDmgBoost": "幻影ダメージ強化",
"physicalDmgBoost": "物理ダメージ強化",
"quantumDmgBoost": "量子ダメージ強化",
"thunderDmgBoost": "雷属性ダメージ強化",
"windDmgBoost": "風属性ダメージ強化",
"pursued": "追加ダメージ",
"true damage": "確定ダメージ",
"elationdamage": "愉悦ダメージ",
"follow-up": "追撃ダメージ",
"elemental damage": "属性ダメージ",
"dot": "継続ダメージ",
"qte": "QTEスキル",
"level": "レベル",
"relics": "遺物",
"eidolons": "エイドロン",
"lightcones": "光円錐",
"loadData": "データ読み込み",
"exportData": "データ出力",
"connectSetting": "接続設定",
"connected": "接続済み",
"unconnected": "未接続",
"psConnection": "PS接続",
"connectionType": "接続タイプ",
"status": "ステータス",
"connectPs": "PS接続",
"disconnect": "切断",
"other": "その他",
"freeSr": "FreeSR",
"database": "データベース",
"enka": "Enka",
"monsterSetting": "モンスター設定",
"serverUrl": "サーバー URL",
"privateType": "プライベートタイプ",
"local": "ローカル",
"server": "サーバー",
"username": "ユーザー名",
"password": "パスワード",
"placeholderServerUrl": "サーバー URL を入力",
"placeholderUsername": "ユーザー名を入力",
"placeholderPassword": "パスワードを入力",
"connectedSuccess": "PS接続成功",
"connectedFailed": "PS接続失敗",
"syncSuccess": "PSへのデータ同期に成功",
"syncFailed": "PSへのデータ同期に失敗",
"sync": "同期",
"importSetting": "インポート設定",
"profile": "プロファイル",
"default": "デフォルト",
"copyProfiles": "プロファイルをコピー",
"addNewProfile": "プロファイルを追加",
"createNewProfile": "新しいプロファイルを作成",
"editProfile": "プロファイル編集",
"placeholderProfileName": "プロファイル名を入力",
"profileName": "プロファイル名",
"create": "作成",
"update": "更新",
"characterInformation": "キャラクター情報",
"skills": "スキル",
"showcaseCard": "ショーケースカード",
"comingSoon": "近日公開",
"characterName": "キャラクター名",
"placeholderCharacter": "キャラクター名を入力",
"characterSettings": "キャラクター設定",
"levelConfiguration": "レベル構成",
"characterLevel": "キャラクターレベル",
"max": "最大",
"ultimateEnergy": "アルティメットエネルギー",
"currentEnergy": "現在のエネルギー",
"setTo50": "50%に設定",
"battleConfiguration": "バトル構成",
"useTechnique": "秘技を使用",
"techniqueNote": "バトル前に秘技効果を有効化",
"enhancement": "強化",
"enhancementLevel": "強化レベル",
"origin": "由来",
"enhancedNote": "強化レベルに応じて追加スキル解放",
"lightconeEquipment": "光円錐装備",
"lightconeSettings": "光円錐の設定",
"placeholderLevel": "レベルを入力",
"superimpositionRank": "重ねランク",
"ranksNote": "ランクが高いほど効果強化",
"changeLightcone": "光円錐を変更",
"removeLightcone": "光円錐を外す",
"equipLightcone": "光円錐を装備",
"noLightconeEquipped": "光円錐が装備されていません",
"equipLightconeNote": "強化のため光円錐を装備してください",
"filter": "フィルター",
"selectedCharacters": "選択中のキャラクター",
"selectedProfiles": "選択中のプロファイル",
"clearAll": "全てクリア",
"selectAll": "全て選択",
"copy": "コピー",
"copied": "コピー済み",
"noAvatarSelected": "キャラクターが選択されていません",
"noAvatarToCopySelected": "コピーするキャラクター選択されていません",
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
"pleaseEnterUid": "UIDを入力してください",
"failedToFetchEnkaData": "Enka のデータ取得に失敗",
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
"noDataToImport": "インポートするデータがありません",
"pleaseSelectAFile": "ファイルを選択してください",
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
"importEnkaDataSuccess": "Enka データのインポート成功",
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
"importDatabaseSuccess": "データベースのインポート成功",
"getData": "データ取得",
"import": "インポート",
"freeSRImport": "FreeSR インポート",
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
"pickAFile": "ファイル選択",
"lightConeSetting": "ライトコーン設定",
"relicMaker": "遺物製作",
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
"relicSavedSuccessfully": "遺物の保存に成功しました",
"mainSettings": "メイン設定",
"mainStat": "メインステータス",
"set": "セット",
"pleaseSelectASet": "セットを選択してください",
"effectBonus": "効果ボーナス",
"totalRoll": "総ロール数",
"randomizeStats": "ステータスランダム化",
"randomizeRolls": "ロール回数ランダム化",
"selectASubStat": "サブステータスを選択",
"selectASet": "セットを選択",
"selectAMainStat": "メインステータスを選んでください",
"save": "保存",
"reset": "リセット",
"roll": "ロール",
"step": "ステップ",
"memoryOfChaos": "混沌の記憶",
"pureFiction": "虚構叙事",
"apocalypticShadow": "末日の幻影",
"customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙",
"floor": "階層",
"side": "前半/後半",
"wave": "ウェーブ",
"stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵",
"firstNodeEnemies": "ノード 1 の敵",
"secondNodeEnemies": "ノード 2 の敵",
"thirdNodeEnemies": "ノード 3 の敵",
"firstNode": "ノード 1",
"secondNode": "ノード 2",
"thirdNode": "ノード 3",
"turbulenceBuff": "乱気流バフ",
"noEventSelected": "イベントが選択されていません",
"noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択",
"selectFloor": "階層を選択",
"selectSide": "前半/後半を選択",
"selectBuff": "バフを選択",
"selectStage": "ステージを選択",
"previous": "前へ",
"next": "次へ",
"noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...",
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
"setEffects": "効果を設定",
"details": "詳細",
"normal": "通常攻撃",
"bpskill": "戦闘スキル",
"assist": "アシストスキル",
"maze": "秘技",
"ultra": "必殺技",
"servantskill": "メモスプライトスキル",
"severaltalent": "メモスプライトの才能",
"singleattack": "単体攻撃",
"enhance": "強化",
"summon": "召喚",
"blast": "爆発",
"restore": "回復",
"support": "支援",
"aoeattack": "範囲攻撃",
"mazeattack": "迷宮秘技",
"impair": "弱体化",
"bounce": "弾跳",
"active": "アクティブ",
"inactive": "非アクティブ",
"defence": "防御",
"maxAll": "すべて最大化",
"maxAllSuccess": "スキルレベルを最大に設定しました。",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。",
"noRelicEquipped": "遺物が装備されていません",
"anomalyArbitration": "異相の仲裁",
"normalMode": "通常モード",
"hardMode": "困難モード",
"selectPEAKEvent": "PEAK イベントを選択",
"mode": "モード",
"selectMode": "モードを選択",
"rollBack": "前の状態に戻る",
"upRoll": "サブステータスを増やす",
"downRoll": "サブステータスを減らす",
"actions": "アクション",
"avatars": "アバター",
"quickView": "クイックビュー",
"extraSetting": "追加設定",
"disableCensorship": "検閲を無効化",
"hideUI": "UIを非表示",
"theoryCraftMode": "シアリークラフトモード",
"cycleCount": "サイクル数",
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
"theoryCraft": "シアリークラフト",
"multipathCharacter": "複数運命キャラ",
"mainPath": "主人公の運命",
"march7Path": "三月なのかの運命",
"challenge": "挑戦",
"skipNode": "ノードをスキップ",
"disableSkip": "スキップ無効",
"skipNode1": "ノード1をスキップ",
"skipNode2": "ノード2をスキップ",
"extraFeatures": "追加機能",
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
"trailblazer": "開拓者",
"listExtraEffect": "追加効果一覧",
"extra": "追加",
"customLineup": "カスタム編成"
}
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "スキルタイプ",
"skillName": "スキル名",
"character": "キャラクター",
"id": "ID",
"path": "運命",
"rarity": "レアリティ",
"element": "属性",
"technique": "秘技",
"talent": "天賦",
"basic": "通常攻撃",
"skill": "スキル",
"ultimate": "アルティメット",
"servant": "サーバント",
"damage": "ダメージ",
"type": "タイプ",
"warrior": "壊滅",
"knight": "存護",
"mage": "知恵",
"priest": "豊穣",
"rogue": "巡狩",
"shaman": "調和",
"warlock": "虚無",
"memory": "記憶",
"elation": "愉悦",
"fire": "火",
"ice": "氷",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
"wind": "風",
"hp": "HP",
"atk": "攻撃",
"speed": "速度",
"critRate": "会心率",
"critDmg": "会心ダメージ",
"breakEffect": "撃破特効",
"effectRes": "効果抵抗",
"energyRegenerationRate": "EP回復効率",
"effectHitRate": "効果命中率",
"outgoingHealingBoost": "回復増加",
"fireDmgBoost": "火属性ダメージ強化",
"iceDmgBoost": "氷属性ダメージ強化",
"imaginaryDmgBoost": "幻影ダメージ強化",
"physicalDmgBoost": "物理ダメージ強化",
"quantumDmgBoost": "量子ダメージ強化",
"thunderDmgBoost": "雷属性ダメージ強化",
"windDmgBoost": "風属性ダメージ強化",
"pursued": "追加ダメージ",
"true damage": "確定ダメージ",
"elationdamage": "愉悦ダメージ",
"follow-up": "追撃ダメージ",
"elemental damage": "属性ダメージ",
"dot": "継続ダメージ",
"qte": "QTEスキル",
"level": "レベル",
"relics": "遺物",
"eidolons": "エイドロン",
"lightcones": "光円錐",
"loadData": "データ読み込み",
"exportData": "データ出力",
"connectSetting": "接続設定",
"connected": "接続済み",
"unconnected": "未接続",
"psConnection": "PS接続",
"connectionType": "接続タイプ",
"status": "ステータス",
"connectPs": "PS接続",
"disconnect": "切断",
"other": "その他",
"freeSr": "FreeSR",
"database": "データベース",
"enka": "Enka",
"monsterSetting": "モンスター設定",
"serverUrl": "サーバー URL",
"privateType": "プライベートタイプ",
"local": "ローカル",
"server": "サーバー",
"username": "ユーザー名",
"password": "パスワード",
"placeholderServerUrl": "サーバー URL を入力",
"placeholderUsername": "ユーザー名を入力",
"placeholderPassword": "パスワードを入力",
"connectedSuccess": "PS接続成功",
"connectedFailed": "PS接続失敗",
"syncSuccess": "PSへのデータ同期に成功",
"syncFailed": "PSへのデータ同期に失敗",
"sync": "同期",
"importSetting": "インポート設定",
"profile": "プロファイル",
"default": "デフォルト",
"copyProfiles": "プロファイルをコピー",
"addNewProfile": "プロファイルを追加",
"createNewProfile": "新しいプロファイルを作成",
"editProfile": "プロファイル編集",
"placeholderProfileName": "プロファイル名を入力",
"profileName": "プロファイル名",
"create": "作成",
"update": "更新",
"characterInformation": "キャラクター情報",
"skills": "スキル",
"showcaseCard": "ショーケースカード",
"comingSoon": "近日公開",
"characterName": "キャラクター名",
"placeholderCharacter": "キャラクター名を入力",
"characterSettings": "キャラクター設定",
"levelConfiguration": "レベル構成",
"characterLevel": "キャラクターレベル",
"max": "最大",
"ultimateEnergy": "アルティメットエネルギー",
"currentEnergy": "現在のエネルギー",
"setTo50": "50%に設定",
"battleConfiguration": "バトル構成",
"useTechnique": "秘技を使用",
"techniqueNote": "バトル前に秘技効果を有効化",
"enhancement": "強化",
"enhancementLevel": "強化レベル",
"origin": "由来",
"enhancedNote": "強化レベルに応じて追加スキル解放",
"lightconeEquipment": "光円錐装備",
"lightconeSettings": "光円錐の設定",
"placeholderLevel": "レベルを入力",
"superimpositionRank": "重ねランク",
"ranksNote": "ランクが高いほど効果強化",
"changeLightcone": "光円錐を変更",
"removeLightcone": "光円錐を外す",
"equipLightcone": "光円錐を装備",
"noLightconeEquipped": "光円錐が装備されていません",
"equipLightconeNote": "強化のため光円錐を装備してください",
"filter": "フィルター",
"selectedCharacters": "選択中のキャラクター",
"selectedProfiles": "選択中のプロファイル",
"clearAll": "全てクリア",
"selectAll": "全て選択",
"copy": "コピー",
"copied": "コピー済み",
"noAvatarSelected": "キャラクターが選択されていません",
"noAvatarToCopySelected": "コピーするキャラクター選択されていません",
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
"pleaseEnterUid": "UIDを入力してください",
"failedToFetchEnkaData": "Enka のデータ取得に失敗",
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
"noDataToImport": "インポートするデータがありません",
"pleaseSelectAFile": "ファイルを選択してください",
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
"importEnkaDataSuccess": "Enka データのインポート成功",
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
"importDatabaseSuccess": "データベースのインポート成功",
"getData": "データ取得",
"import": "インポート",
"freeSRImport": "FreeSR インポート",
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
"pickAFile": "ファイル選択",
"lightConeSetting": "ライトコーン設定",
"relicMaker": "遺物製作",
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
"relicSavedSuccessfully": "遺物の保存に成功しました",
"mainSettings": "メイン設定",
"mainStat": "メインステータス",
"set": "セット",
"pleaseSelectASet": "セットを選択してください",
"effectBonus": "効果ボーナス",
"totalRoll": "総ロール数",
"randomizeStats": "ステータスランダム化",
"randomizeRolls": "ロール回数ランダム化",
"selectASubStat": "サブステータスを選択",
"selectASet": "セットを選択",
"selectAMainStat": "メインステータスを選んでください",
"save": "保存",
"reset": "リセット",
"roll": "ロール",
"step": "ステップ",
"memoryOfChaos": "混沌の記憶",
"pureFiction": "虚構叙事",
"apocalypticShadow": "末日の幻影",
"customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙",
"floor": "階層",
"side": "前半/後半",
"wave": "ウェーブ",
"stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵",
"firstNodeEnemies": "ノード 1 の敵",
"secondNodeEnemies": "ノード 2 の敵",
"thirdNodeEnemies": "ノード 3 の敵",
"firstNode": "ノード 1",
"secondNode": "ノード 2",
"thirdNode": "ノード 3",
"turbulenceBuff": "乱気流バフ",
"noEventSelected": "イベントが選択されていません",
"noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択",
"selectFloor": "階層を選択",
"selectSide": "前半/後半を選択",
"selectBuff": "バフを選択",
"selectStage": "ステージを選択",
"previous": "前へ",
"next": "次へ",
"noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...",
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
"setEffects": "効果を設定",
"details": "詳細",
"normal": "通常攻撃",
"bpskill": "戦闘スキル",
"assist": "アシストスキル",
"maze": "秘技",
"ultra": "必殺技",
"servantskill": "メモスプライトスキル",
"severaltalent": "メモスプライトの才能",
"singleattack": "単体攻撃",
"enhance": "強化",
"summon": "召喚",
"blast": "爆発",
"restore": "回復",
"support": "支援",
"aoeattack": "範囲攻撃",
"mazeattack": "迷宮秘技",
"impair": "弱体化",
"bounce": "弾跳",
"active": "アクティブ",
"inactive": "非アクティブ",
"defence": "防御",
"maxAll": "すべて最大化",
"maxAllSuccess": "スキルレベルを最大に設定しました。",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。",
"noRelicEquipped": "遺物が装備されていません",
"anomalyArbitration": "異相の仲裁",
"normalMode": "通常モード",
"hardMode": "困難モード",
"selectPEAKEvent": "PEAK イベントを選択",
"mode": "モード",
"selectMode": "モードを選択",
"rollBack": "前の状態に戻る",
"upRoll": "サブステータスを増やす",
"downRoll": "サブステータスを減らす",
"actions": "アクション",
"avatars": "アバター",
"quickView": "クイックビュー",
"extraSetting": "追加設定",
"disableCensorship": "検閲を無効化",
"hideUI": "UIを非表示",
"theoryCraftMode": "シアリークラフトモード",
"cycleCount": "サイクル数",
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
"theoryCraft": "シアリークラフト",
"multipathCharacter": "複数運命キャラ",
"mainPath": "主人公の運命",
"march7Path": "三月なのかの運命",
"challenge": "挑戦",
"skipNode": "ノードをスキップ",
"disableSkip": "スキップ無効",
"skipNode1": "ノード1をスキップ",
"skipNode2": "ノード2をスキップ",
"extraFeatures": "追加機能",
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
"trailblazer": "開拓者",
"listExtraEffect": "追加効果一覧",
"extra": "追加",
"customLineup": "カスタム編成",
"def": "防御力",
"language": "言語"
}
}
+292 -290
View File
@@ -1,290 +1,292 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "스킬 유형",
"skillName": "스킬 이름",
"character": "캐릭터",
"id": "ID",
"path": "운명",
"rarity": "희귀도",
"element": "속성",
"technique": "비기",
"talent": "탈렌트",
"basic": "기본 공격",
"skill": "스킬",
"ultimate": "궁극기",
"servant": "서번트",
"damage": "데미지",
"type": "종류",
"warrior": "파괴",
"knight": "보호",
"mage": "박학",
"priest": "풍요",
"rogue": "사냥",
"shaman": "조화",
"warlock": "허무",
"memory": "기억",
"elation": "환락",
"fire": "화염",
"ice": "얼음",
"imaginary": "환상",
"physical": "물리",
"quantum": "양자",
"thunder": "번개",
"wind": "바람",
"hp": "HP",
"atk": "공격력",
"speed": "속도",
"critRate": "치명타 확률",
"critDmg": "치명타 피해",
"breakEffect": "파괴 피해",
"effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복속도",
"effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유 강화",
"fireDmgBoost": "화염 피해 증가",
"iceDmgBoost": "얼음 피해 증가",
"imaginaryDmgBoost": "환상 피해 증가",
"physicalDmgBoost": "물리 피해 증가",
"quantumDmgBoost": "양자 피해 증가",
"thunderDmgBoost": "번개 피해 증가",
"windDmgBoost": "바람 피해 증가",
"pursued": "추가 피해",
"true damage": "진실한 피해",
"elationdamage": "환락 피해",
"follow-up": "후속 피해",
"elemental damage": "파괴 및 초파괴 피해",
"dot": "시간 경과 피해",
"qte": "QTE 스킬",
"level": "레벨",
"relics": "유물",
"eidolons": "에이돌론",
"lightcones": "라이트콘",
"loadData": "데이터 불러오기",
"exportData": "데이터 내보내기",
"connectSetting": "연결 설정",
"connected": "연결됨",
"unconnected": "연결 안됨",
"psConnection": "PS 연결",
"connectionType": "연결 타입",
"status": "상태",
"connectPs": "PS 연결",
"disconnect": "연결 끊기",
"other": "기타",
"freeSr": "FreeSR",
"database": "데이터베이스",
"enka": "Enka",
"monsterSetting": "몬스터 설정",
"serverUrl": "서버 URL",
"privateType": "프라이빗 타입",
"local": "로컬",
"server": "서버",
"username": "사용자명",
"password": "비밀번호",
"placeholderServerUrl": "서버 URL 입력",
"placeholderUsername": "사용자명 입력",
"placeholderPassword": "비밀번호 입력",
"connectedSuccess": "PS에 성공적으로 연결됨",
"connectedFailed": "PS 연결 실패",
"syncSuccess": "PS에 데이터 동기화 성공",
"syncFailed": "PS에 데이터 동기화 실패",
"sync": "동기화",
"importSetting": "설정 가져오기",
"profile": "프로필",
"default": "기본",
"copyProfiles": "프로필 복사",
"addNewProfile": "새 프로필 추가",
"createNewProfile": "새 프로필 생성",
"editProfile": "프로필 편집",
"placeholderProfileName": "프로필 이름 입력",
"profileName": "프로필 이름",
"create": "생성",
"update": "업데이트",
"characterInformation": "캐릭터 정보",
"skills": "스킬",
"showcaseCard": "쇼케이스 카드",
"comingSoon": "곧 출시",
"characterName": "캐릭터 이름",
"placeholderCharacter": "캐릭터 이름 입력",
"characterSettings": "캐릭터 설정",
"levelConfiguration": "레벨 구성",
"characterLevel": "캐릭터 레벨",
"max": "최대",
"ultimateEnergy": "궁극 에너지",
"currentEnergy": "현재 에너지",
"setTo50": "50%로 설정",
"battleConfiguration": "전투 구성",
"useTechnique": "비기 사용",
"techniqueNote": "전투 전 비기 효과 활성화",
"enhancement": "강화",
"enhancementLevel": "강화 레벨",
"origin": "출처",
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
"lightconeEquipment": "라이트콘 장비",
"lightconeSettings": "라이트콘 설정",
"placeholderLevel": "레벨 입력",
"superimpositionRank": "중첩 등급",
"ranksNote": "등급이 높을수록 효과가 강함",
"changeLightcone": "라이트콘 변경",
"removeLightcone": "라이트콘 제거",
"equipLightcone": "라이트콘 장착",
"noLightconeEquipped": "라이트콘 미장착",
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
"filter": "필터",
"selectedCharacters": "선택된 캐릭터",
"selectedProfiles": "선택된 프로필",
"clearAll": "전체 지우기",
"selectAll": "전체 선택",
"copy": "복사",
"copied": "복사됨",
"noAvatarSelected": "캐릭터가 선택되지 않음",
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
"pleaseEnterUid": "UID를 입력해주세요",
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
"noDataToImport": "가져올 데이터가 없습니다",
"pleaseSelectAFile": "파일을 선택하세요",
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
"getData": "데이터 가져오기",
"import": "가져오기",
"freeSRImport": "FreeSR 가져오기",
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
"pickAFile": "파일 선택",
"lightConeSetting": "라이트콘 설정",
"relicMaker": "유물 제작",
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
"relicSavedSuccessfully": "유물 저장 성공",
"mainSettings": "메인 설정",
"mainStat": "주 속성",
"set": "세트",
"pleaseSelectASet": "세트 하나를 선택해주세요",
"effectBonus": "효과 보너스",
"totalRoll": "총 횟수",
"randomizeStats": "속성 랜덤화",
"randomizeRolls": "횟수 랜덤화",
"selectASubStat": "부 속성 선택",
"selectASet": "세트 선택",
"selectAMainStat": "주 속성 선택",
"save": "저장",
"reset": "초기화",
"roll": "굴리기",
"step": "단계",
"memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수",
"side": "전반/후반",
"wave": "웨이브",
"stage": "스테이지",
"useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적",
"firstNodeEnemies": "노드 1 적",
"secondNodeEnemies": "노드 2 적",
"thirdNodeEnemies": "노드 3 적",
"firstNode": "노드 1",
"secondNode": "노드 2",
"thirdNode": "노드 3",
"turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반",
"lower": "하반",
"upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택",
"selectFloor": "층 선택",
"selectSide": "전반/후반 선택",
"selectBuff": "버프 선택",
"selectStage": "스테이지 선택",
"previous": "이전",
"next": "다음",
"noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...",
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정",
"details": "상세",
"normal": "기본 공격",
"bpskill": "스킬",
"assist": "지원 스킬",
"maze": "전술",
"ultra": "필살기",
"servantskill": "메모스프라이트 스킬",
"severaltalent": "메모스프라이트 재능",
"singleattack": "단일 공격",
"enhance": "강화",
"summon": "소환",
"blast": "광역 공격",
"restore": "회복",
"support": "지원",
"aoeattack": "광역 공격",
"mazeattack": "미궁 비기",
"impair": "약화",
"bounce": "튕김",
"active": "활성",
"inactive": "비활성",
"defence": "방어",
"maxAll": "모두 최대치",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
"noRelicEquipped": "성유물이 장착되지 않음",
"anomalyArbitration": "이상 중재",
"normalMode": "일반 모드",
"hardMode": "어려움 모드",
"selectPEAKEvent": "PEAK 이벤트 선택",
"mode": "모드",
"selectMode": "모드를 선택",
"rollBack": "이전 상태로 되돌리기",
"upRoll": "부옵션 추가",
"downRoll": "부옵션 감소",
"actions": "동작",
"avatars": "아바타",
"quickView": "빠른 조회",
"extraSetting": "추가 설정",
"disableCensorship": "검열 비활성화",
"hideUI": "UI 숨기기",
"theoryCraftMode": "Theory Craft 모드",
"cycleCount": "사이클 수",
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
"theoryCraft": "Theory Craft",
"multipathCharacter": "다중 운명 캐릭터",
"mainPath": "개척자 운명",
"march7Path": "삼월칠일 운명",
"challenge": "도전",
"skipNode": "노드 건너뛰기",
"disableSkip": "건너뛰기 비활성화",
"skipNode1": "노드 1 건너뛰기",
"skipNode2": "노드 2 건너뛰기",
"extraFeatures": "추가 기능",
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
"trailblazer": "개척자",
"listExtraEffect": "추가 효과 목록",
"extra": "추가",
"customLineup": "커스텀 편성"
}
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "스킬 유형",
"skillName": "스킬 이름",
"character": "캐릭터",
"id": "ID",
"path": "운명",
"rarity": "희귀도",
"element": "속성",
"technique": "비기",
"talent": "탈렌트",
"basic": "기본 공격",
"skill": "스킬",
"ultimate": "궁극기",
"servant": "서번트",
"damage": "데미지",
"type": "종류",
"warrior": "파괴",
"knight": "보호",
"mage": "박학",
"priest": "풍요",
"rogue": "사냥",
"shaman": "조화",
"warlock": "허무",
"memory": "기억",
"elation": "환락",
"fire": "화염",
"ice": "얼음",
"imaginary": "환상",
"physical": "물리",
"quantum": "양자",
"thunder": "번개",
"wind": "바람",
"hp": "HP",
"atk": "공격력",
"speed": "속도",
"critRate": "치명타 확률",
"critDmg": "치명타 피해",
"breakEffect": "파괴 피해",
"effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복속도",
"effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유 강화",
"fireDmgBoost": "화염 피해 증가",
"iceDmgBoost": "얼음 피해 증가",
"imaginaryDmgBoost": "환상 피해 증가",
"physicalDmgBoost": "물리 피해 증가",
"quantumDmgBoost": "양자 피해 증가",
"thunderDmgBoost": "번개 피해 증가",
"windDmgBoost": "바람 피해 증가",
"pursued": "추가 피해",
"true damage": "진실한 피해",
"elationdamage": "환락 피해",
"follow-up": "후속 피해",
"elemental damage": "파괴 및 초파괴 피해",
"dot": "시간 경과 피해",
"qte": "QTE 스킬",
"level": "레벨",
"relics": "유물",
"eidolons": "에이돌론",
"lightcones": "라이트콘",
"loadData": "데이터 불러오기",
"exportData": "데이터 내보내기",
"connectSetting": "연결 설정",
"connected": "연결됨",
"unconnected": "연결 안됨",
"psConnection": "PS 연결",
"connectionType": "연결 타입",
"status": "상태",
"connectPs": "PS 연결",
"disconnect": "연결 끊기",
"other": "기타",
"freeSr": "FreeSR",
"database": "데이터베이스",
"enka": "Enka",
"monsterSetting": "몬스터 설정",
"serverUrl": "서버 URL",
"privateType": "프라이빗 타입",
"local": "로컬",
"server": "서버",
"username": "사용자명",
"password": "비밀번호",
"placeholderServerUrl": "서버 URL 입력",
"placeholderUsername": "사용자명 입력",
"placeholderPassword": "비밀번호 입력",
"connectedSuccess": "PS에 성공적으로 연결됨",
"connectedFailed": "PS 연결 실패",
"syncSuccess": "PS에 데이터 동기화 성공",
"syncFailed": "PS에 데이터 동기화 실패",
"sync": "동기화",
"importSetting": "설정 가져오기",
"profile": "프로필",
"default": "기본",
"copyProfiles": "프로필 복사",
"addNewProfile": "새 프로필 추가",
"createNewProfile": "새 프로필 생성",
"editProfile": "프로필 편집",
"placeholderProfileName": "프로필 이름 입력",
"profileName": "프로필 이름",
"create": "생성",
"update": "업데이트",
"characterInformation": "캐릭터 정보",
"skills": "스킬",
"showcaseCard": "쇼케이스 카드",
"comingSoon": "곧 출시",
"characterName": "캐릭터 이름",
"placeholderCharacter": "캐릭터 이름 입력",
"characterSettings": "캐릭터 설정",
"levelConfiguration": "레벨 구성",
"characterLevel": "캐릭터 레벨",
"max": "최대",
"ultimateEnergy": "궁극 에너지",
"currentEnergy": "현재 에너지",
"setTo50": "50%로 설정",
"battleConfiguration": "전투 구성",
"useTechnique": "비기 사용",
"techniqueNote": "전투 전 비기 효과 활성화",
"enhancement": "강화",
"enhancementLevel": "강화 레벨",
"origin": "출처",
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
"lightconeEquipment": "라이트콘 장비",
"lightconeSettings": "라이트콘 설정",
"placeholderLevel": "레벨 입력",
"superimpositionRank": "중첩 등급",
"ranksNote": "등급이 높을수록 효과가 강함",
"changeLightcone": "라이트콘 변경",
"removeLightcone": "라이트콘 제거",
"equipLightcone": "라이트콘 장착",
"noLightconeEquipped": "라이트콘 미장착",
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
"filter": "필터",
"selectedCharacters": "선택된 캐릭터",
"selectedProfiles": "선택된 프로필",
"clearAll": "전체 지우기",
"selectAll": "전체 선택",
"copy": "복사",
"copied": "복사됨",
"noAvatarSelected": "캐릭터가 선택되지 않음",
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
"pleaseEnterUid": "UID를 입력해주세요",
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
"noDataToImport": "가져올 데이터가 없습니다",
"pleaseSelectAFile": "파일을 선택하세요",
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
"getData": "데이터 가져오기",
"import": "가져오기",
"freeSRImport": "FreeSR 가져오기",
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
"pickAFile": "파일 선택",
"lightConeSetting": "라이트콘 설정",
"relicMaker": "유물 제작",
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
"relicSavedSuccessfully": "유물 저장 성공",
"mainSettings": "메인 설정",
"mainStat": "주 속성",
"set": "세트",
"pleaseSelectASet": "세트 하나를 선택해주세요",
"effectBonus": "효과 보너스",
"totalRoll": "총 횟수",
"randomizeStats": "속성 랜덤화",
"randomizeRolls": "횟수 랜덤화",
"selectASubStat": "부 속성 선택",
"selectASet": "세트 선택",
"selectAMainStat": "주 속성 선택",
"save": "저장",
"reset": "초기화",
"roll": "굴리기",
"step": "단계",
"memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수",
"side": "전반/후반",
"wave": "웨이브",
"stage": "스테이지",
"useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적",
"firstNodeEnemies": "노드 1 적",
"secondNodeEnemies": "노드 2 적",
"thirdNodeEnemies": "노드 3 적",
"firstNode": "노드 1",
"secondNode": "노드 2",
"thirdNode": "노드 3",
"turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반",
"lower": "하반",
"upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택",
"selectFloor": "층 선택",
"selectSide": "전반/후반 선택",
"selectBuff": "버프 선택",
"selectStage": "스테이지 선택",
"previous": "이전",
"next": "다음",
"noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...",
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정",
"details": "상세",
"normal": "기본 공격",
"bpskill": "스킬",
"assist": "지원 스킬",
"maze": "전술",
"ultra": "필살기",
"servantskill": "메모스프라이트 스킬",
"severaltalent": "메모스프라이트 재능",
"singleattack": "단일 공격",
"enhance": "강화",
"summon": "소환",
"blast": "광역 공격",
"restore": "회복",
"support": "지원",
"aoeattack": "광역 공격",
"mazeattack": "미궁 비기",
"impair": "약화",
"bounce": "튕김",
"active": "활성",
"inactive": "비활성",
"defence": "방어",
"maxAll": "모두 최대치",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
"noRelicEquipped": "성유물이 장착되지 않음",
"anomalyArbitration": "이상 중재",
"normalMode": "일반 모드",
"hardMode": "어려움 모드",
"selectPEAKEvent": "PEAK 이벤트 선택",
"mode": "모드",
"selectMode": "모드를 선택",
"rollBack": "이전 상태로 되돌리기",
"upRoll": "부옵션 추가",
"downRoll": "부옵션 감소",
"actions": "동작",
"avatars": "아바타",
"quickView": "빠른 조회",
"extraSetting": "추가 설정",
"disableCensorship": "검열 비활성화",
"hideUI": "UI 숨기기",
"theoryCraftMode": "Theory Craft 모드",
"cycleCount": "사이클 수",
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
"theoryCraft": "Theory Craft",
"multipathCharacter": "다중 운명 캐릭터",
"mainPath": "개척자 운명",
"march7Path": "삼월칠일 운명",
"challenge": "도전",
"skipNode": "노드 건너뛰기",
"disableSkip": "건너뛰기 비활성화",
"skipNode1": "노드 1 건너뛰기",
"skipNode2": "노드 2 건너뛰기",
"extraFeatures": "추가 기능",
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
"trailblazer": "개척자",
"listExtraEffect": "추가 효과 목록",
"extra": "추가",
"customLineup": "커스텀 편성",
"def": "방어력",
"language": "언어"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Ferramentas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de Habilidade",
"skillName": "Nome da Habilidade",
"character": "Personagem",
"id": "Id",
"path": "Caminho",
"rarity": "Raridade",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Perícia",
"ultimate": "Perícia Suprema",
"servant": "Servo",
"damage": "Dano",
"type": "Tipo",
"warrior": "A Destruição",
"knight": "A Preservação",
"mage": "A Erudição",
"priest": "A Abundância",
"rogue": "A Caça",
"shaman": "A Harmonia",
"warlock": "A Inexistência",
"memory": "A Lembrança",
"elation": "A Alegria",
"fire": "Fogo",
"ice": "Gelo",
"imaginary": "Imaginário",
"physical": "Físico",
"quantum": "Quântico",
"thunder": "Raio",
"wind": "Vento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Taxa CRIT",
"critDmg": "Dano CRIT",
"breakEffect": "Efeito de Quebra",
"effectRes": "RES a Efeito",
"energyRegenerationRate": "Taxa de Regeneração de Energia",
"effectHitRate": "Taxa de Acerto de Efeito",
"outgoingHealingBoost": "Bônus de Cura",
"fireDmgBoost": "Bônus de Dano de Fogo",
"iceDmgBoost": "Bônus de Dano de Gelo",
"imaginaryDmgBoost": "Bônus de Dano Imaginário",
"physicalDmgBoost": "Bônus de Dano Físico",
"quantumDmgBoost": "Bônus de Dano Quântico",
"thunderDmgBoost": "Bônus de Dano de Raio",
"windDmgBoost": "Bônus de Dano de Vento",
"pursued": "Dano Adicional",
"true damage": "Dano Verdadeiro",
"elationdamage": "Dano de Alegria",
"follow-up": "Dano de Ataque Extra",
"elemental damage": "Dano de Quebra e Superquebra",
"dot": "Dano Contínuo",
"qte": "Habilidade QTE",
"level": "Nível",
"relics": "Relíquias",
"eidolons": "Eidolons",
"lightcones": "Cones de Luz",
"loadData": "Carregar dados",
"exportData": "Exportar dados",
"connectSetting": "Configuração de Conexão",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexão PS",
"connectionType": "Tipo de Conexão",
"status": "Status",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Outro",
"freeSr": "FreeSR",
"database": "Banco de dados",
"enka": "Enka",
"monsterSetting": "Configuração de Monstros",
"serverUrl": "URL do Servidor",
"privateType": "Tipo Privado",
"local": "Local",
"server": "Servidor",
"username": "Usuário",
"password": "Senha",
"placeholderServerUrl": "Digite o URL do servidor",
"placeholderUsername": "Digite o usuário",
"placeholderPassword": "Digite a senha",
"connectedSuccess": "Conectado ao PS com sucesso",
"connectedFailed": "Falha ao conectar ao PS",
"syncSuccess": "Dados sincronizados com o PS com sucesso",
"syncFailed": "Falha ao sincronizar dados com o PS",
"sync": "Sincronizar",
"importSetting": "Configuração de Importação",
"profile": "Perfil",
"default": "Padrão",
"copyProfiles": "Copiar perfis",
"addNewProfile": "Adicionar novo perfil",
"createNewProfile": "Criar novo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Digite o nome do perfil",
"profileName": "Nome do perfil",
"create": "Criar",
"update": "Atualizar",
"characterInformation": "Informação do Personagem",
"skills": "Habilidades",
"showcaseCard": "Cartão de Exibição",
"comingSoon": "Em breve",
"characterName": "Nome do personagem",
"placeholderCharacter": "Digite o nome do personagem",
"characterSettings": "Configurações de Personagem",
"levelConfiguration": "Configuração de Nível",
"characterLevel": "Nível do Personagem",
"max": "MÁX",
"ultimateEnergy": "Energia da Perícia Suprema",
"currentEnergy": "Energia Atual",
"setTo50": "Definir para 50%",
"battleConfiguration": "Configuração de Batalha",
"useTechnique": "Usar Técnica",
"techniqueNote": "Ativar efeitos de técnica pré-batalha",
"enhancement": "Aprimoramento",
"enhancementLevel": "Nível de Aprimoramento",
"origin": "Origem",
"enhancedNote": "Níveis altos desbloqueiam habilidades",
"lightconeEquipment": "Equipamento de Cone de Luz",
"lightconeSettings": "Configurações de Cone de Luz",
"placeholderLevel": "Digite o nível",
"superimpositionRank": "Rank de Sobreposição",
"ranksNote": "Ranks maiores dão efeitos mais fortes",
"changeLightcone": "Mudar Cone de Luz",
"removeLightcone": "Remover Cone de Luz",
"equipLightcone": "Equipar Cone de Luz",
"noLightconeEquipped": "Sem Cone de Luz",
"equipLightconeNote": "Equipe um cone de luz para melhorar o personagem",
"filter": "Filtro",
"selectedCharacters": "Personagens Selecionados",
"selectedProfiles": "Perfis Selecionados",
"clearAll": "Limpar Tudo",
"selectAll": "Selecionar Tudo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Nenhum personagem selecionado",
"noAvatarToCopySelected": "Nenhum personagem para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil",
"pleaseEnterUid": "Por favor, insira o UID",
"failedToFetchEnkaData": "Falha ao obter dados Enka",
"pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem",
"noDataToImport": "Sem dados para importar",
"pleaseSelectAFile": "Por favor, selecione um arquivo",
"fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido",
"importEnkaDataSuccess": "Dados Enka importados com sucesso",
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
"importDatabaseSuccess": "Banco de dados importado com sucesso",
"getData": "Obter Dados",
"import": "Importar",
"freeSRImport": "Importação FreeSR",
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
"pickAFile": "Escolher um arquivo",
"lightConeSetting": "Configuração de Cone de Luz",
"relicMaker": "Criador de Relíquias",
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
"relicSavedSuccessfully": "Relíquia salva com sucesso",
"mainSettings": "Configurações Principais",
"mainStat": "Atributo Principal",
"set": "Conjunto",
"pleaseSelectASet": "Selecione um conjunto",
"effectBonus": "Bônus de Efeito",
"totalRoll": "Total de Melhorias",
"randomizeStats": "Atributos Aleatórios",
"randomizeRolls": "Melhorias Aleatórias",
"selectASubStat": "Selecionar sub-atributo",
"selectASet": "Selecionar conjunto",
"selectAMainStat": "Selecionar atributo principal",
"save": "Salvar",
"reset": "Redefinir",
"roll": "Melhoria",
"step": "Passo",
"memoryOfChaos": "Memória do Caos",
"pureFiction": "Pura Ficção",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Inimigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Andar",
"side": "Lado",
"wave": "Onda",
"stage": "Fase",
"useCycleCount": "Usar contagem de ciclos?",
"useTurbulenceBuff": "Usar buff de turbulência?",
"firstHalfEnemies": "Inimigos da primeira metade",
"secondHalfEnemies": "Inimigos da segunda metade",
"firstNodeEnemies": "Inimigos do nodo 1",
"secondNodeEnemies": "Inimigos do nodo 2",
"thirdNodeEnemies": "Inimigos do nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de inimigos",
"turbulenceBuff": "Buff de Turbulência",
"noEventSelected": "Nenhum evento selecionado",
"noTurbulenceBuff": "Sem Buff de Turbulência",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Selecionar Evento MOC",
"selectPFEvent": "Selecionar Evento PF",
"selectASEvent": "Selecionar Evento AS",
"selectCEEvent": "Selecionar Evento CE",
"selectEvent": "Selecionar Evento",
"selectFloor": "Selecionar Andar",
"selectSide": "Selecionar Lado",
"selectBuff": "Selecionar Buff",
"selectStage": "Selecionar Fase",
"previous": "Anterior",
"next": "Próximo",
"noMonstersFound": "Nenhum monstro encontrado",
"addNewWave": "Adicionar Nova Onda",
"searchStage": "Buscar fase...",
"noStageFound": "Fase não encontrada",
"searchMonster": "Buscar monstro...",
"changeRelic": "Mudar relíquia",
"deleteRelic": "Deletar relíquia",
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
"setEffects": "Definir Efeitos",
"details": "Detalhes",
"normal": "Ataque Básico",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Ferramentas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de Habilidade",
"skillName": "Nome da Habilidade",
"character": "Personagem",
"id": "Id",
"path": "Caminho",
"rarity": "Raridade",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Perícia",
"ultimate": "Perícia Suprema",
"servant": "Servo",
"damage": "Dano",
"type": "Tipo",
"warrior": "A Destruição",
"knight": "A Preservação",
"mage": "A Erudição",
"priest": "A Abundância",
"rogue": "A Caça",
"shaman": "A Harmonia",
"warlock": "A Inexistência",
"memory": "A Lembrança",
"elation": "A Alegria",
"fire": "Fogo",
"ice": "Gelo",
"imaginary": "Imaginário",
"physical": "Físico",
"quantum": "Quântico",
"thunder": "Raio",
"wind": "Vento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Taxa CRIT",
"critDmg": "Dano CRIT",
"breakEffect": "Efeito de Quebra",
"effectRes": "RES a Efeito",
"energyRegenerationRate": "Taxa de Regeneração de Energia",
"effectHitRate": "Taxa de Acerto de Efeito",
"outgoingHealingBoost": "Bônus de Cura",
"fireDmgBoost": "Bônus de Dano de Fogo",
"iceDmgBoost": "Bônus de Dano de Gelo",
"imaginaryDmgBoost": "Bônus de Dano Imaginário",
"physicalDmgBoost": "Bônus de Dano Físico",
"quantumDmgBoost": "Bônus de Dano Quântico",
"thunderDmgBoost": "Bônus de Dano de Raio",
"windDmgBoost": "Bônus de Dano de Vento",
"pursued": "Dano Adicional",
"true damage": "Dano Verdadeiro",
"elationdamage": "Dano de Alegria",
"follow-up": "Dano de Ataque Extra",
"elemental damage": "Dano de Quebra e Superquebra",
"dot": "Dano Contínuo",
"qte": "Habilidade QTE",
"level": "Nível",
"relics": "Relíquias",
"eidolons": "Eidolons",
"lightcones": "Cones de Luz",
"loadData": "Carregar dados",
"exportData": "Exportar dados",
"connectSetting": "Configuração de Conexão",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexão PS",
"connectionType": "Tipo de Conexão",
"status": "Status",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Outro",
"freeSr": "FreeSR",
"database": "Banco de dados",
"enka": "Enka",
"monsterSetting": "Configuração de Monstros",
"serverUrl": "URL do Servidor",
"privateType": "Tipo Privado",
"local": "Local",
"server": "Servidor",
"username": "Usuário",
"password": "Senha",
"placeholderServerUrl": "Digite o URL do servidor",
"placeholderUsername": "Digite o usuário",
"placeholderPassword": "Digite a senha",
"connectedSuccess": "Conectado ao PS com sucesso",
"connectedFailed": "Falha ao conectar ao PS",
"syncSuccess": "Dados sincronizados com o PS com sucesso",
"syncFailed": "Falha ao sincronizar dados com o PS",
"sync": "Sincronizar",
"importSetting": "Configuração de Importação",
"profile": "Perfil",
"default": "Padrão",
"copyProfiles": "Copiar perfis",
"addNewProfile": "Adicionar novo perfil",
"createNewProfile": "Criar novo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Digite o nome do perfil",
"profileName": "Nome do perfil",
"create": "Criar",
"update": "Atualizar",
"characterInformation": "Informação do Personagem",
"skills": "Habilidades",
"showcaseCard": "Cartão de Exibição",
"comingSoon": "Em breve",
"characterName": "Nome do personagem",
"placeholderCharacter": "Digite o nome do personagem",
"characterSettings": "Configurações de Personagem",
"levelConfiguration": "Configuração de Nível",
"characterLevel": "Nível do Personagem",
"max": "MÁX",
"ultimateEnergy": "Energia da Perícia Suprema",
"currentEnergy": "Energia Atual",
"setTo50": "Definir para 50%",
"battleConfiguration": "Configuração de Batalha",
"useTechnique": "Usar Técnica",
"techniqueNote": "Ativar efeitos de técnica pré-batalha",
"enhancement": "Aprimoramento",
"enhancementLevel": "Nível de Aprimoramento",
"origin": "Origem",
"enhancedNote": "Níveis altos desbloqueiam habilidades",
"lightconeEquipment": "Equipamento de Cone de Luz",
"lightconeSettings": "Configurações de Cone de Luz",
"placeholderLevel": "Digite o nível",
"superimpositionRank": "Rank de Sobreposição",
"ranksNote": "Ranks maiores dão efeitos mais fortes",
"changeLightcone": "Mudar Cone de Luz",
"removeLightcone": "Remover Cone de Luz",
"equipLightcone": "Equipar Cone de Luz",
"noLightconeEquipped": "Sem Cone de Luz",
"equipLightconeNote": "Equipe um cone de luz para melhorar o personagem",
"filter": "Filtro",
"selectedCharacters": "Personagens Selecionados",
"selectedProfiles": "Perfis Selecionados",
"clearAll": "Limpar Tudo",
"selectAll": "Selecionar Tudo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Nenhum personagem selecionado",
"noAvatarToCopySelected": "Nenhum personagem para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil",
"pleaseEnterUid": "Por favor, insira o UID",
"failedToFetchEnkaData": "Falha ao obter dados Enka",
"pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem",
"noDataToImport": "Sem dados para importar",
"pleaseSelectAFile": "Por favor, selecione um arquivo",
"fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido",
"importEnkaDataSuccess": "Dados Enka importados com sucesso",
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
"importDatabaseSuccess": "Banco de dados importado com sucesso",
"getData": "Obter Dados",
"import": "Importar",
"freeSRImport": "Importação FreeSR",
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
"pickAFile": "Escolher um arquivo",
"lightConeSetting": "Configuração de Cone de Luz",
"relicMaker": "Criador de Relíquias",
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
"relicSavedSuccessfully": "Relíquia salva com sucesso",
"mainSettings": "Configurações Principais",
"mainStat": "Atributo Principal",
"set": "Conjunto",
"pleaseSelectASet": "Selecione um conjunto",
"effectBonus": "Bônus de Efeito",
"totalRoll": "Total de Melhorias",
"randomizeStats": "Atributos Aleatórios",
"randomizeRolls": "Melhorias Aleatórias",
"selectASubStat": "Selecionar sub-atributo",
"selectASet": "Selecionar conjunto",
"selectAMainStat": "Selecionar atributo principal",
"save": "Salvar",
"reset": "Redefinir",
"roll": "Melhoria",
"step": "Passo",
"memoryOfChaos": "Memória do Caos",
"pureFiction": "Pura Ficção",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Inimigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Andar",
"side": "Lado",
"wave": "Onda",
"stage": "Fase",
"useCycleCount": "Usar contagem de ciclos?",
"useTurbulenceBuff": "Usar buff de turbulência?",
"firstHalfEnemies": "Inimigos da primeira metade",
"secondHalfEnemies": "Inimigos da segunda metade",
"firstNodeEnemies": "Inimigos do nodo 1",
"secondNodeEnemies": "Inimigos do nodo 2",
"thirdNodeEnemies": "Inimigos do nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de inimigos",
"turbulenceBuff": "Buff de Turbulência",
"noEventSelected": "Nenhum evento selecionado",
"noTurbulenceBuff": "Sem Buff de Turbulência",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Selecionar Evento MOC",
"selectPFEvent": "Selecionar Evento PF",
"selectASEvent": "Selecionar Evento AS",
"selectCEEvent": "Selecionar Evento CE",
"selectEvent": "Selecionar Evento",
"selectFloor": "Selecionar Andar",
"selectSide": "Selecionar Lado",
"selectBuff": "Selecionar Buff",
"selectStage": "Selecionar Fase",
"previous": "Anterior",
"next": "Próximo",
"noMonstersFound": "Nenhum monstro encontrado",
"addNewWave": "Adicionar Nova Onda",
"searchStage": "Buscar fase...",
"noStageFound": "Fase não encontrada",
"searchMonster": "Buscar monstro...",
"changeRelic": "Mudar relíquia",
"deleteRelic": "Deletar relíquia",
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
"setEffects": "Definir Efeitos",
"details": "Detalhes",
"normal": "Ataque Básico",
"bpskill": "Perícia",
"maze": "Técnica",
"ultra": "Perícia Suprema",
"assist": "Habilidade de Assistência",
"servantskill": "Habilidade do Memoespirito",
"severaltalent": "Talento do Memoespirito",
"singleattack": "Ataque Único",
"enhance": "Aprimorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Explosão",
"restore": "Restaurar",
"support": "Suporte",
"aoeattack": "Ataque em Área",
"impair": "Enfraquecer",
"bounce": "Rebote",
"active": "Ativo",
"defence": "Defesa",
"inactive": "Inativo",
"maxAll": "Maximizar Tudo",
"maxAllSuccess": "Habilidades maximizadas com sucesso.",
"maxAllFailed": "Falha ao maximizar habilidades.",
"noRelicEquipped": "Sem relíquia equipada",
"anomalyArbitration": "Arbitragem de Anomalia",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Selecionar Evento PEAK",
"mode": "Modo",
"selectMode": "Selecionar um modo",
"rollBack": "Reverter",
"upRoll": "Aumentar Melhoria",
"downRoll": "Diminuir Melhoria",
"actions": "Ações",
"avatars": "Avatares",
"quickView": "Visualização Rápida",
"extraSetting": "Configurações Extras",
"disableCensorship": "Desativar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Contagem de Ciclos",
"pleaseSelectAllSubStats": "Selecione todos os sub-atributos",
"subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personagem Multicaminho",
"mainPath": "Caminho Principal",
"march7Path": "Caminho de 7 de Março",
"challenge": "Desafio",
"skipNode": "Pular Nó",
"disableSkip": "Desativar pulo",
"skipNode1": "Pular nó 1",
"skipNode2": "Pular nó 2",
"extraFeatures": "Recursos Extras",
"detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.",
"detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.",
"detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.",
"detailHiddenUi": "Irá esconder a interface do jogo.",
"detailDisableCensorship": "Desativa a censura no jogo.",
"detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.",
"trailblazer": "Desbravador",
"listExtraEffect": "Lista de Efeitos Extras",
"extra": "Extra",
"customLineup": "Formação Personalizada"
}
"severaltalent": "Talento do Memoespirito",
"singleattack": "Ataque Único",
"enhance": "Aprimorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Explosão",
"restore": "Restaurar",
"support": "Suporte",
"aoeattack": "Ataque em Área",
"impair": "Enfraquecer",
"bounce": "Rebote",
"active": "Ativo",
"defence": "Defesa",
"inactive": "Inativo",
"maxAll": "Maximizar Tudo",
"maxAllSuccess": "Habilidades maximizadas com sucesso.",
"maxAllFailed": "Falha ao maximizar habilidades.",
"noRelicEquipped": "Sem relíquia equipada",
"anomalyArbitration": "Arbitragem de Anomalia",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Selecionar Evento PEAK",
"mode": "Modo",
"selectMode": "Selecionar um modo",
"rollBack": "Reverter",
"upRoll": "Aumentar Melhoria",
"downRoll": "Diminuir Melhoria",
"actions": "Ações",
"avatars": "Avatares",
"quickView": "Visualização Rápida",
"extraSetting": "Configurações Extras",
"disableCensorship": "Desativar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Contagem de Ciclos",
"pleaseSelectAllSubStats": "Selecione todos os sub-atributos",
"subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personagem Multicaminho",
"mainPath": "Caminho Principal",
"march7Path": "Caminho de 7 de Março",
"challenge": "Desafio",
"skipNode": "Pular Nó",
"disableSkip": "Desativar pulo",
"skipNode1": "Pular nó 1",
"skipNode2": "Pular nó 2",
"extraFeatures": "Recursos Extras",
"detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.",
"detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.",
"detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.",
"detailHiddenUi": "Irá esconder a interface do jogo.",
"detailDisableCensorship": "Desativa a censura no jogo.",
"detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.",
"trailblazer": "Desbravador",
"listExtraEffect": "Lista de Efeitos Extras",
"extra": "Extra",
"customLineup": "Formação Personalizada",
"def": "DEF",
"language": "Idioma"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Инструменты Firefly от Firefly Shelter"
},
"DataPage": {
"skillType": "Тип навыка",
"skillName": "Название навыка",
"character": "Персонаж",
"id": "ID",
"path": "Путь",
"rarity": "Редкость",
"element": "Элемент",
"technique": "Техника",
"talent": "Талант",
"basic": "Базовая атака",
"skill": "Навык",
"ultimate": "Сверхспособность",
"servant": "Слуга",
"damage": "Урон",
"type": "Тип",
"warrior": "Разрушение",
"knight": "Сохранение",
"mage": "Эрудиция",
"priest": "Изобилие",
"rogue": "Охота",
"shaman": "Гармония",
"warlock": "Небытие",
"memory": "Память",
"elation": "Радость",
"fire": "Огонь",
"ice": "Лед",
"imaginary": "Мнимый",
"physical": "Физический",
"quantum": "Квантовый",
"thunder": "Электрический",
"wind": "Ветряной",
"hp": "HP",
"atk": "Сила атаки",
"speed": "Скорость",
"critRate": "Крит. шанс",
"critDmg": "Крит. урон",
"breakEffect": "Эффект пробития",
"effectRes": "Сопротивление эффектам",
"energyRegenerationRate": "Скорость восст. энергии",
"effectHitRate": "Шанс попадания эффектов",
"outgoingHealingBoost": "Бонус исходящего исцеления",
"fireDmgBoost": "Бонус огненного урона",
"iceDmgBoost": "Бонус ледяного урона",
"imaginaryDmgBoost": "Бонус мнимого урона",
"physicalDmgBoost": "Бонус физ. урона",
"quantumDmgBoost": "Бонус квантового урона",
"thunderDmgBoost": "Бонус электр. урона",
"windDmgBoost": "Бонус ветряного урона",
"pursued": "Дополнительный урон",
"true damage": "Истинный урон",
"elationdamage": "Урон радости",
"follow-up": "Урон бонус-атаки",
"elemental damage": "Урон пробития и суперпробития",
"dot": "Периодический урон",
"qte": "QTE Навык",
"level": "Уровень",
"relics": "Реликвии",
"eidolons": "Эйдолоны",
"lightcones": "Световые конусы",
"loadData": "Загрузить данные",
"exportData": "Экспортировать данные",
"connectSetting": "Настройки подключения",
"connected": "Подключено",
"unconnected": "Не подключено",
"psConnection": "Подключение к PS",
"connectionType": "Тип подключения",
"status": "Статус",
"connectPs": "Подключить PS",
"disconnect": "Отключить",
"other": "Другое",
"freeSr": "FreeSR",
"database": "База данных",
"enka": "Enka",
"monsterSetting": "Настройки монстров",
"serverUrl": "URL сервера",
"privateType": "Приватный тип",
"local": "Локальный",
"server": "Сервер",
"username": "Имя пользователя",
"password": "Пароль",
"placeholderServerUrl": "Введите URL сервера",
"placeholderUsername": "Введите имя пользователя",
"placeholderPassword": "Введите пароль",
"connectedSuccess": "Успешное подключение к PS",
"connectedFailed": "Ошибка подключения к PS",
"syncSuccess": "Данные успешно синхронизированы с PS",
"syncFailed": "Ошибка синхронизации данных",
"sync": "Синхронизация",
"importSetting": "Настройки импорта",
"profile": "Профиль",
"default": "По умолчанию",
"copyProfiles": "Копировать профили",
"addNewProfile": "Добавить новый профиль",
"createNewProfile": "Создать новый профиль",
"editProfile": "Редактировать профиль",
"placeholderProfileName": "Введите имя профиля",
"profileName": "Имя профиля",
"create": "Создать",
"update": "Обновить",
"characterInformation": "Информация о персонаже",
"skills": "Навыки",
"showcaseCard": "Карточка персонажа",
"comingSoon": "Скоро",
"characterName": "Имя персонажа",
"placeholderCharacter": "Введите имя персонажа",
"characterSettings": "Настройки персонажа",
"levelConfiguration": "Конфигурация уровня",
"characterLevel": "Уровень персонажа",
"max": "МАКС",
"ultimateEnergy": "Энергия сверхспособности",
"currentEnergy": "Текущая энергия",
"setTo50": "Установить на 50%",
"battleConfiguration": "Конфигурация боя",
"useTechnique": "Использовать технику",
"techniqueNote": "Включить эффекты техники до боя",
"enhancement": "Усиление",
"enhancementLevel": "Уровень усиления",
"origin": "Оригинал",
"enhancedNote": "Высокие уровни открывают новые способности",
"lightconeEquipment": "Снаряжение конуса",
"lightconeSettings": "Настройки светового конуса",
"placeholderLevel": "Введите уровень",
"superimpositionRank": "Уровень наложения",
"ranksNote": "Высокие ранги дают более сильные эффекты",
"changeLightcone": "Изменить световой конус",
"removeLightcone": "Снять световой конус",
"equipLightcone": "Экипировать конус",
"noLightconeEquipped": "Световой конус не экипирован",
"equipLightconeNote": "Наденьте конус для усиления персонажа",
"filter": "Фильтр",
"selectedCharacters": "Выбранные персонажи",
"selectedProfiles": "Выбранные профили",
"clearAll": "Очистить всё",
"selectAll": "Выбрать всё",
"copy": "Копировать",
"copied": "Скопировано",
"noAvatarSelected": "Персонаж не выбран",
"noAvatarToCopySelected": "Нет персонажа для копирования",
"pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль",
"pleaseEnterUid": "Пожалуйста, введите UID",
"failedToFetchEnkaData": "Не удалось получить данные Enka",
"pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа",
"noDataToImport": "Нет данных для импорта",
"pleaseSelectAFile": "Пожалуйста, выберите файл",
"fileMustBeAValidJsonFile": "Файл должен быть в формате JSON",
"importEnkaDataSuccess": "Успешный импорт из Enka",
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
"importDatabaseSuccess": "Успешный импорт базы данных",
"getData": "Получить данные",
"import": "Импорт",
"freeSRImport": "Импорт FreeSR",
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
"pickAFile": "Выберите файл",
"lightConeSetting": "Настройки конуса",
"relicMaker": "Создатель реликвий",
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
"relicSavedSuccessfully": "Реликвия успешно сохранена",
"mainSettings": "Основные настройки",
"mainStat": "Основная характеристика",
"set": "Набор",
"pleaseSelectASet": "Выберите набор",
"effectBonus": "Бонус эффекта",
"totalRoll": "Всего улучшений",
"randomizeStats": "Случайные статы",
"randomizeRolls": "Случайные улучшения",
"selectASubStat": "Выберите доп. стат",
"selectASet": "Выберите набор",
"selectAMainStat": "Выберите основной стат",
"save": "Сохранить",
"reset": "Сброс",
"roll": "Улучшение",
"step": "Шаг",
"memoryOfChaos": "Зал забвения",
"pureFiction": "Чистый вымысел",
"apocalypticShadow": "Апокалиптическая тень",
"customEnemy": "Свой противник",
"simulatedUniverse": "Виртуальная вселенная",
"floor": "Этаж",
"side": "Половина",
"wave": "Волна",
"stage": "Стадия",
"useCycleCount": "Использовать счетчик циклов?",
"useTurbulenceBuff": "Использовать бафф турбулентности?",
"firstHalfEnemies": "Враги первой половины",
"secondHalfEnemies": "Враги второй половины",
"firstNodeEnemies": "Враги узла 1",
"secondNodeEnemies": "Враги узла 2",
"thirdNodeEnemies": "Враги узла 3",
"firstNode": "Узел 1",
"secondNode": "Узел 2",
"thirdNode": "Узел 3",
"listEnemies": "Список врагов",
"turbulenceBuff": "Бафф турбулентности",
"noEventSelected": "Событие не выбрано",
"noTurbulenceBuff": "Нет баффа турбулентности",
"upper": "Верх",
"lower": "Низ",
"upperToLower": "Верх -> Низ",
"lowerToUpper": "Низ -> Верх",
"selectMOCEvent": "Выбрать событие MOC",
"selectPFEvent": "Выбрать событие PF",
"selectASEvent": "Выбрать событие AS",
"selectCEEvent": "Выбрать событие CE",
"selectEvent": "Выбрать событие",
"selectFloor": "Выбрать этаж",
"selectSide": "Выбрать половину",
"selectBuff": "Выбрать бафф",
"selectStage": "Выбрать стадию",
"previous": "Назад",
"next": "Вперед",
"noMonstersFound": "Монстры не найдены",
"addNewWave": "Добавить новую волну",
"searchStage": "Поиск стадии...",
"noStageFound": "Стадия не найдена",
"searchMonster": "Поиск монстра...",
"changeRelic": "Изменить реликвию",
"deleteRelic": "Удалить реликвию",
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
"setEffects": "Настроить эффекты",
"details": "Детали",
"normal": "Базовая атака",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Инструменты Firefly от Firefly Shelter"
},
"DataPage": {
"skillType": "Тип навыка",
"skillName": "Название навыка",
"character": "Персонаж",
"id": "ID",
"path": "Путь",
"rarity": "Редкость",
"element": "Элемент",
"technique": "Техника",
"talent": "Талант",
"basic": "Базовая атака",
"skill": "Навык",
"ultimate": "Сверхспособность",
"servant": "Слуга",
"damage": "Урон",
"type": "Тип",
"warrior": "Разрушение",
"knight": "Сохранение",
"mage": "Эрудиция",
"priest": "Изобилие",
"rogue": "Охота",
"shaman": "Гармония",
"warlock": "Небытие",
"memory": "Память",
"elation": "Радость",
"fire": "Огонь",
"ice": "Лед",
"imaginary": "Мнимый",
"physical": "Физический",
"quantum": "Квантовый",
"thunder": "Электрический",
"wind": "Ветряной",
"hp": "HP",
"atk": "Сила атаки",
"speed": "Скорость",
"critRate": "Крит. шанс",
"critDmg": "Крит. урон",
"breakEffect": "Эффект пробития",
"effectRes": "Сопротивление эффектам",
"energyRegenerationRate": "Скорость восст. энергии",
"effectHitRate": "Шанс попадания эффектов",
"outgoingHealingBoost": "Бонус исходящего исцеления",
"fireDmgBoost": "Бонус огненного урона",
"iceDmgBoost": "Бонус ледяного урона",
"imaginaryDmgBoost": "Бонус мнимого урона",
"physicalDmgBoost": "Бонус физ. урона",
"quantumDmgBoost": "Бонус квантового урона",
"thunderDmgBoost": "Бонус электр. урона",
"windDmgBoost": "Бонус ветряного урона",
"pursued": "Дополнительный урон",
"true damage": "Истинный урон",
"elationdamage": "Урон радости",
"follow-up": "Урон бонус-атаки",
"elemental damage": "Урон пробития и суперпробития",
"dot": "Периодический урон",
"qte": "QTE Навык",
"level": "Уровень",
"relics": "Реликвии",
"eidolons": "Эйдолоны",
"lightcones": "Световые конусы",
"loadData": "Загрузить данные",
"exportData": "Экспортировать данные",
"connectSetting": "Настройки подключения",
"connected": "Подключено",
"unconnected": "Не подключено",
"psConnection": "Подключение к PS",
"connectionType": "Тип подключения",
"status": "Статус",
"connectPs": "Подключить PS",
"disconnect": "Отключить",
"other": "Другое",
"freeSr": "FreeSR",
"database": "База данных",
"enka": "Enka",
"monsterSetting": "Настройки монстров",
"serverUrl": "URL сервера",
"privateType": "Приватный тип",
"local": "Локальный",
"server": "Сервер",
"username": "Имя пользователя",
"password": "Пароль",
"placeholderServerUrl": "Введите URL сервера",
"placeholderUsername": "Введите имя пользователя",
"placeholderPassword": "Введите пароль",
"connectedSuccess": "Успешное подключение к PS",
"connectedFailed": "Ошибка подключения к PS",
"syncSuccess": "Данные успешно синхронизированы с PS",
"syncFailed": "Ошибка синхронизации данных",
"sync": "Синхронизация",
"importSetting": "Настройки импорта",
"profile": "Профиль",
"default": "По умолчанию",
"copyProfiles": "Копировать профили",
"addNewProfile": "Добавить новый профиль",
"createNewProfile": "Создать новый профиль",
"editProfile": "Редактировать профиль",
"placeholderProfileName": "Введите имя профиля",
"profileName": "Имя профиля",
"create": "Создать",
"update": "Обновить",
"characterInformation": "Информация о персонаже",
"skills": "Навыки",
"showcaseCard": "Карточка персонажа",
"comingSoon": "Скоро",
"characterName": "Имя персонажа",
"placeholderCharacter": "Введите имя персонажа",
"characterSettings": "Настройки персонажа",
"levelConfiguration": "Конфигурация уровня",
"characterLevel": "Уровень персонажа",
"max": "МАКС",
"ultimateEnergy": "Энергия сверхспособности",
"currentEnergy": "Текущая энергия",
"setTo50": "Установить на 50%",
"battleConfiguration": "Конфигурация боя",
"useTechnique": "Использовать технику",
"techniqueNote": "Включить эффекты техники до боя",
"enhancement": "Усиление",
"enhancementLevel": "Уровень усиления",
"origin": "Оригинал",
"enhancedNote": "Высокие уровни открывают новые способности",
"lightconeEquipment": "Снаряжение конуса",
"lightconeSettings": "Настройки светового конуса",
"placeholderLevel": "Введите уровень",
"superimpositionRank": "Уровень наложения",
"ranksNote": "Высокие ранги дают более сильные эффекты",
"changeLightcone": "Изменить световой конус",
"removeLightcone": "Снять световой конус",
"equipLightcone": "Экипировать конус",
"noLightconeEquipped": "Световой конус не экипирован",
"equipLightconeNote": "Наденьте конус для усиления персонажа",
"filter": "Фильтр",
"selectedCharacters": "Выбранные персонажи",
"selectedProfiles": "Выбранные профили",
"clearAll": "Очистить всё",
"selectAll": "Выбрать всё",
"copy": "Копировать",
"copied": "Скопировано",
"noAvatarSelected": "Персонаж не выбран",
"noAvatarToCopySelected": "Нет персонажа для копирования",
"pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль",
"pleaseEnterUid": "Пожалуйста, введите UID",
"failedToFetchEnkaData": "Не удалось получить данные Enka",
"pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа",
"noDataToImport": "Нет данных для импорта",
"pleaseSelectAFile": "Пожалуйста, выберите файл",
"fileMustBeAValidJsonFile": "Файл должен быть в формате JSON",
"importEnkaDataSuccess": "Успешный импорт из Enka",
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
"importDatabaseSuccess": "Успешный импорт базы данных",
"getData": "Получить данные",
"import": "Импорт",
"freeSRImport": "Импорт FreeSR",
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
"pickAFile": "Выберите файл",
"lightConeSetting": "Настройки конуса",
"relicMaker": "Создатель реликвий",
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
"relicSavedSuccessfully": "Реликвия успешно сохранена",
"mainSettings": "Основные настройки",
"mainStat": "Основная характеристика",
"set": "Набор",
"pleaseSelectASet": "Выберите набор",
"effectBonus": "Бонус эффекта",
"totalRoll": "Всего улучшений",
"randomizeStats": "Случайные статы",
"randomizeRolls": "Случайные улучшения",
"selectASubStat": "Выберите доп. стат",
"selectASet": "Выберите набор",
"selectAMainStat": "Выберите основной стат",
"save": "Сохранить",
"reset": "Сброс",
"roll": "Улучшение",
"step": "Шаг",
"memoryOfChaos": "Зал забвения",
"pureFiction": "Чистый вымысел",
"apocalypticShadow": "Апокалиптическая тень",
"customEnemy": "Свой противник",
"simulatedUniverse": "Виртуальная вселенная",
"floor": "Этаж",
"side": "Половина",
"wave": "Волна",
"stage": "Стадия",
"useCycleCount": "Использовать счетчик циклов?",
"useTurbulenceBuff": "Использовать бафф турбулентности?",
"firstHalfEnemies": "Враги первой половины",
"secondHalfEnemies": "Враги второй половины",
"firstNodeEnemies": "Враги узла 1",
"secondNodeEnemies": "Враги узла 2",
"thirdNodeEnemies": "Враги узла 3",
"firstNode": "Узел 1",
"secondNode": "Узел 2",
"thirdNode": "Узел 3",
"listEnemies": "Список врагов",
"turbulenceBuff": "Бафф турбулентности",
"noEventSelected": "Событие не выбрано",
"noTurbulenceBuff": "Нет баффа турбулентности",
"upper": "Верх",
"lower": "Низ",
"upperToLower": "Верх -> Низ",
"lowerToUpper": "Низ -> Верх",
"selectMOCEvent": "Выбрать событие MOC",
"selectPFEvent": "Выбрать событие PF",
"selectASEvent": "Выбрать событие AS",
"selectCEEvent": "Выбрать событие CE",
"selectEvent": "Выбрать событие",
"selectFloor": "Выбрать этаж",
"selectSide": "Выбрать половину",
"selectBuff": "Выбрать бафф",
"selectStage": "Выбрать стадию",
"previous": "Назад",
"next": "Вперед",
"noMonstersFound": "Монстры не найдены",
"addNewWave": "Добавить новую волну",
"searchStage": "Поиск стадии...",
"noStageFound": "Стадия не найдена",
"searchMonster": "Поиск монстра...",
"changeRelic": "Изменить реликвию",
"deleteRelic": "Удалить реликвию",
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
"setEffects": "Настроить эффекты",
"details": "Детали",
"normal": "Базовая атака",
"bpskill": "Навык",
"maze": "Техника",
"ultra": "Сверхспособность",
"assist": "Навык поддержки",
"servantskill": "Навык мемоспрайта",
"severaltalent": "Талант мемоспрайта",
"singleattack": "Одиночная атака",
"enhance": "Усиление",
"summon": "Призыв",
"mazeattack": "Атака в технике",
"blast": "Взрыв",
"restore": "Восстановление",
"support": "Поддержка",
"aoeattack": "АоЕ атака",
"impair": "Ослабление",
"bounce": "Отскок",
"active": "Активен",
"defence": "Защита",
"inactive": "Неактивен",
"maxAll": "Улучшить всё",
"maxAllSuccess": "Уровни навыков улучшены до максимума.",
"maxAllFailed": "Не удалось улучшить навыки.",
"noRelicEquipped": "Нет экипированных реликвий",
"anomalyArbitration": "Аномальный арбитраж",
"normalMode": "Обычный режим",
"hardMode": "Сложный режим",
"selectPEAKEvent": "Выбрать событие PEAK",
"mode": "Режим",
"selectMode": "Выбрать режим",
"rollBack": "Отменить шаг",
"upRoll": "Повысить улучшение",
"downRoll": "Понизить улучшение",
"actions": "Действия",
"avatars": "Персонажи",
"quickView": "Быстрый просмотр",
"extraSetting": "Дополнительные настройки",
"disableCensorship": "Отключить цензуру",
"hideUI": "Скрыть интерфейс",
"theoryCraftMode": "Режим Theorycraft",
"cycleCount": "Счетчик циклов",
"pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы",
"subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Многопутевой персонаж",
"mainPath": "Основной Путь",
"march7Path": "Путь Март 7",
"challenge": "Испытание",
"skipNode": "Пропустить узел",
"disableSkip": "Отключить пропуск",
"skipNode1": "Пропустить узел 1",
"skipNode2": "Пропустить узел 2",
"extraFeatures": "Дополнительные функции",
"detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.",
"detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.",
"detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.",
"detailHiddenUi": "Скрывает игровой интерфейс.",
"detailDisableCensorship": "Отключает внутриигровую цензуру.",
"detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.",
"trailblazer": "Первопроходец",
"listExtraEffect": "Список доп. эффектов",
"extra": "Экстра",
"customLineup": "Пользовательский отряд"
}
"severaltalent": "Талант мемоспрайта",
"singleattack": "Одиночная атака",
"enhance": "Усиление",
"summon": "Призыв",
"mazeattack": "Атака в технике",
"blast": "Взрыв",
"restore": "Восстановление",
"support": "Поддержка",
"aoeattack": "АоЕ атака",
"impair": "Ослабление",
"bounce": "Отскок",
"active": "Активен",
"defence": "Защита",
"inactive": "Неактивен",
"maxAll": "Улучшить всё",
"maxAllSuccess": "Уровни навыков улучшены до максимума.",
"maxAllFailed": "Не удалось улучшить навыки.",
"noRelicEquipped": "Нет экипированных реликвий",
"anomalyArbitration": "Аномальный арбитраж",
"normalMode": "Обычный режим",
"hardMode": "Сложный режим",
"selectPEAKEvent": "Выбрать событие PEAK",
"mode": "Режим",
"selectMode": "Выбрать режим",
"rollBack": "Отменить шаг",
"upRoll": "Повысить улучшение",
"downRoll": "Понизить улучшение",
"actions": "Действия",
"avatars": "Персонажи",
"quickView": "Быстрый просмотр",
"extraSetting": "Дополнительные настройки",
"disableCensorship": "Отключить цензуру",
"hideUI": "Скрыть интерфейс",
"theoryCraftMode": "Режим Theorycraft",
"cycleCount": "Счетчик циклов",
"pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы",
"subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Многопутевой персонаж",
"mainPath": "Основной Путь",
"march7Path": "Путь Март 7",
"challenge": "Испытание",
"skipNode": "Пропустить узел",
"disableSkip": "Отключить пропуск",
"skipNode1": "Пропустить узел 1",
"skipNode2": "Пропустить узел 2",
"extraFeatures": "Дополнительные функции",
"detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.",
"detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.",
"detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.",
"detailHiddenUi": "Скрывает игровой интерфейс.",
"detailDisableCensorship": "Отключает внутриигровую цензуру.",
"detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.",
"trailblazer": "Первопроходец",
"listExtraEffect": "Список доп. эффектов",
"extra": "Экстра",
"customLineup": "Пользовательский отряд",
"def": "Защ",
"language": "Язык"
}
}
+287 -285
View File
@@ -1,291 +1,293 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "เครื่องมือ Firefly โดย Firefly Shelter"
},
"DataPage": {
"skillType": "ประเภทสกิล",
"skillName": "ชื่อสกิล",
"character": "ตัวละคร",
"id": "ไอดี",
"path": "Path",
"rarity": "ระดับความหายาก",
"element": "ธาตุ",
"technique": "เทคนิค",
"talent": "พรสวรรค์",
"basic": "โจมตีปกติ",
"skill": "สกิลต่อสู้",
"ultimate": "ท่าไม้ตาย",
"servant": "ผู้ติดตาม",
"damage": "ความเสียหาย",
"type": "ประเภท",
"warrior": "ทำลายล้าง",
"knight": "อนุรักษ์",
"mage": "ปัญญา",
"priest": "เฟื่องฟู",
"rogue": "ล่าสังหาร",
"shaman": "ประสาน",
"warlock": "ลบล้าง",
"memory": "ลบล้าง",
"elation": "ปิติสุข",
"fire": "ไฟ",
"ice": "น้ำแข็ง",
"imaginary": "จินตภาพ",
"physical": "กายภาพ",
"quantum": "ควอนตัม",
"thunder": "สายฟ้า",
"wind": "ลม",
"hp": "HP",
"atk": "ATK",
"speed": "ความเร็ว",
"critRate": "อัตราคริติคอล",
"critDmg": "ความเสียหายคริติคอล",
"breakEffect": "เอฟเฟกต์ทำลายล้าง",
"effectRes": "ต้านทานสถานะ",
"energyRegenerationRate": "อัตราฟื้นฟูพลังงาน",
"effectHitRate": "อัตราสร้างสถานะ",
"outgoingHealingBoost": "โบนัสการรักษา",
"fireDmgBoost": "โบนัสความเสียหายไฟ",
"iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง",
"imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ",
"physicalDmgBoost": "โบนัสความเสียหายกายภาพ",
"quantumDmgBoost": "โบนัสความเสียหายควอนตัม",
"thunderDmgBoost": "โบนัสความเสียหายสายฟ้า",
"windDmgBoost": "โบนัสความเสียหายลม",
"pursued": "ความเสียหายเพิ่มเติม",
"true damage": "ความเสียหายจริง",
"elationdamage": "ความเสียหายปิติสุข",
"follow-up": "ความเสียหายโจมตีต่อเนื่อง",
"elemental damage": "ความเสียหายทำลายล้างและซูเปอร์",
"dot": "ความเสียหายต่อเนื่อง",
"qte": "สกิล QTE",
"level": "เลเวล",
"relics": "รีลิกส์",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "โหลดข้อมูล",
"exportData": "ส่งออกข้อมูล",
"connectSetting": "การตั้งค่าการเชื่อมต่อ",
"connected": "เชื่อมต่อแล้ว",
"unconnected": "ไม่ได้เชื่อมต่อ",
"psConnection": "การเชื่อมต่อ PS",
"connectionType": "ประเภทการเชื่อมต่อ",
"status": "สถานะ",
"connectPs": "เชื่อมต่อ PS",
"disconnect": "ยกเลิกการเชื่อมต่อ",
"other": "อื่นๆ",
"freeSr": "FreeSR",
"database": "ฐานข้อมูล",
"enka": "Enka",
"monsterSetting": "ตั้งค่ามอนสเตอร์",
"serverUrl": "URL เซิร์ฟเวอร์",
"privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว",
"local": "ในเครื่อง",
"server": "เซิร์ฟเวอร์",
"username": "ชื่อผู้ใช้",
"password": "รหัสผ่าน",
"placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์",
"placeholderUsername": "ป้อนชื่อผู้ใช้",
"placeholderPassword": "ป้อนรหัสผ่าน",
"connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ",
"connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS",
"syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ",
"syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS",
"sync": "ซิงค์",
"importSetting": "การตั้งค่าการนำเข้า",
"profile": "โปรไฟล์",
"default": "ค่าเริ่มต้น",
"copyProfiles": "คัดลอกโปรไฟล์",
"addNewProfile": "เพิ่มโปรไฟล์ใหม่",
"createNewProfile": "สร้างโปรไฟล์ใหม่",
"editProfile": "แก้ไขโปรไฟล์",
"placeholderProfileName": "ป้อนชื่อโปรไฟล์",
"profileName": "ชื่อโปรไฟล์",
"create": "สร้าง",
"update": "อัปเดต",
"characterInformation": "ข้อมูลตัวละคร",
"skills": "สกิล",
"showcaseCard": "การ์ดแสดง",
"comingSoon": "เร็วๆ นี้",
"characterName": "ชื่อตัวละคร",
"placeholderCharacter": "ป้อนชื่อตัวละคร",
"characterSettings": "การตั้งค่าตัวละคร",
"levelConfiguration": "การกำหนดระดับเลเวล",
"characterLevel": "เลเวลตัวละคร",
"max": "สูงสุด",
"ultimateEnergy": "พลังงานท่าไม้ตาย",
"currentEnergy": "พลังงานปัจจุบัน",
"setTo50": "ตั้งค่าเป็น 50%",
"battleConfiguration": "การกำหนดค่าการต่อสู้",
"useTechnique": "ใช้เทคนิค",
"techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้",
"enhancement": "การเสริมพลัง",
"enhancementLevel": "ระดับการเสริมพลัง",
"origin": "ดั้งเดิม",
"enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ",
"lightconeEquipment": "สวมใส่ Light Cone",
"lightconeSettings": "การตั้งค่า Light Cone",
"placeholderLevel": "ป้อนเลเวล",
"superimpositionRank": "ระดับการขัดเกลา",
"ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า",
"changeLightcone": "เปลี่ยน Light Cone",
"removeLightcone": "ถอด Light Cone",
"equipLightcone": "สวมใส่ Light Cone",
"noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone",
"equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง",
"filter": "กรอง",
"selectedCharacters": "ตัวละครที่เลือก",
"selectedProfiles": "โปรไฟล์ที่เลือก",
"clearAll": "ล้างทั้งหมด",
"selectAll": "เลือกทั้งหมด",
"copy": "คัดลอก",
"copied": "คัดลอกแล้ว",
"noAvatarSelected": "ไม่ได้เลือกตัวละคร",
"noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก",
"pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์",
"pleaseEnterUid": "โปรดป้อน UID",
"failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้",
"pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร",
"noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า",
"pleaseSelectAFile": "โปรดเลือกไฟล์",
"fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง",
"importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ",
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
"getData": "รับข้อมูล",
"import": "นำเข้า",
"freeSRImport": "นำเข้า FreeSR",
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
"pickAFile": "เลือกไฟล์",
"lightConeSetting": "การตั้งค่า Light Cone",
"relicMaker": "สร้างรีลิกส์",
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
"relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ",
"mainSettings": "การตั้งค่าหลัก",
"mainStat": "ค่าสเตตัสหลัก",
"set": "เซ็ต",
"pleaseSelectASet": "โปรดเลือกเซ็ต",
"effectBonus": "โบนัสเอฟเฟกต์",
"totalRoll": "จำนวนการอัปเกรด",
"randomizeStats": "สุ่มสเตตัส",
"randomizeRolls": "สุ่มอัปเกรด",
"selectASubStat": "เลือกสเตตัสรอง",
"selectASet": "เลือกเซ็ต",
"selectAMainStat": "เลือกสเตตัสหลัก",
"save": "บันทึก",
"reset": "รีเซ็ต",
"roll": "อัปเกรด",
"step": "ขั้น",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "ศัตรูกำหนดเอง",
"simulatedUniverse": "Simulated Universe",
"floor": "ชั้น",
"side": "ฝั่ง",
"wave": "เวฟ",
"stage": "ด่าน",
"useCycleCount": "ใช้การนับรอบหรือไม่?",
"useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?",
"firstHalfEnemies": "ศัตรูครึ่งแรก",
"secondHalfEnemies": "ศัตรูครึ่งหลัง",
"firstNodeEnemies": "ศัตรูโหนด 1",
"secondNodeEnemies": "ศัตรูโหนด 2",
"thirdNodeEnemies": "ศัตรูโหนด 3",
"firstNode": "โหนด 1",
"secondNode": "โหนด 2",
"thirdNode": "โหนด 3",
"listEnemies": "รายการศัตรู",
"turbulenceBuff": "บัฟบรรยากาศ",
"noEventSelected": "ไม่ได้เลือกอีเวนต์",
"noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ",
"upper": "บน",
"lower": "ล่าง",
"upperToLower": "บน -> ล่าง",
"lowerToUpper": "ล่าง -> บน",
"selectMOCEvent": "เลือกอีเวนต์ MOC",
"selectPFEvent": "เลือกอีเวนต์ PF",
"selectASEvent": "เลือกอีเวนต์ AS",
"selectCEEvent": "เลือกอีเวนต์ CE",
"selectEvent": "เลือกอีเวนต์",
"selectFloor": "เลือกชั้น",
"selectSide": "เลือกฝั่ง",
"selectBuff": "เลือกบัฟ",
"selectStage": "เลือกด่าน",
"previous": "ก่อนหน้า",
"next": "ถัดไป",
"noMonstersFound": "ไม่พบมอนสเตอร์",
"addNewWave": "เพิ่มเวฟใหม่",
"searchStage": "ค้นหาด่าน...",
"noStageFound": "ไม่พบด่าน",
"searchMonster": "ค้นหามอนสเตอร์...",
"changeRelic": "เปลี่ยนรีลิกส์",
"deleteRelic": "ลบรีลิกส์",
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
"setEffects": "ตั้งค่าเอฟเฟกต์",
"details": "รายละเอียด",
"normal": "โจมตีปกติ",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "เครื่องมือ Firefly โดย Firefly Shelter"
},
"DataPage": {
"skillType": "ประเภทสกิล",
"skillName": "ชื่อสกิล",
"character": "ตัวละคร",
"id": "ไอดี",
"path": "Path",
"rarity": "ระดับความหายาก",
"element": "ธาตุ",
"technique": "เทคนิค",
"talent": "พรสวรรค์",
"basic": "โจมตีปกติ",
"skill": "สกิลต่อสู้",
"ultimate": "ท่าไม้ตาย",
"servant": "ผู้ติดตาม",
"damage": "ความเสียหาย",
"type": "ประเภท",
"warrior": "ทำลายล้าง",
"knight": "อนุรักษ์",
"mage": "ปัญญา",
"priest": "เฟื่องฟู",
"rogue": "ล่าสังหาร",
"shaman": "ประสาน",
"warlock": "ลบล้าง",
"memory": "ลบล้าง",
"elation": "ปิติสุข",
"fire": "ไฟ",
"ice": "น้ำแข็ง",
"imaginary": "จินตภาพ",
"physical": "กายภาพ",
"quantum": "ควอนตัม",
"thunder": "สายฟ้า",
"wind": "ลม",
"hp": "HP",
"atk": "ATK",
"speed": "ความเร็ว",
"critRate": "อัตราคริติคอล",
"critDmg": "ความเสียหายคริติคอล",
"breakEffect": "เอฟเฟกต์ทำลายล้าง",
"effectRes": "ต้านทานสถานะ",
"energyRegenerationRate": "อัตราฟื้นฟูพลังงาน",
"effectHitRate": "อัตราสร้างสถานะ",
"outgoingHealingBoost": "โบนัสการรักษา",
"fireDmgBoost": "โบนัสความเสียหายไฟ",
"iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง",
"imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ",
"physicalDmgBoost": "โบนัสความเสียหายกายภาพ",
"quantumDmgBoost": "โบนัสความเสียหายควอนตัม",
"thunderDmgBoost": "โบนัสความเสียหายสายฟ้า",
"windDmgBoost": "โบนัสความเสียหายลม",
"pursued": "ความเสียหายเพิ่มเติม",
"true damage": "ความเสียหายจริง",
"elationdamage": "ความเสียหายปิติสุข",
"follow-up": "ความเสียหายโจมตีต่อเนื่อง",
"elemental damage": "ความเสียหายทำลายล้างและซูเปอร์",
"dot": "ความเสียหายต่อเนื่อง",
"qte": "สกิล QTE",
"level": "เลเวล",
"relics": "รีลิกส์",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "โหลดข้อมูล",
"exportData": "ส่งออกข้อมูล",
"connectSetting": "การตั้งค่าการเชื่อมต่อ",
"connected": "เชื่อมต่อแล้ว",
"unconnected": "ไม่ได้เชื่อมต่อ",
"psConnection": "การเชื่อมต่อ PS",
"connectionType": "ประเภทการเชื่อมต่อ",
"status": "สถานะ",
"connectPs": "เชื่อมต่อ PS",
"disconnect": "ยกเลิกการเชื่อมต่อ",
"other": "อื่นๆ",
"freeSr": "FreeSR",
"database": "ฐานข้อมูล",
"enka": "Enka",
"monsterSetting": "ตั้งค่ามอนสเตอร์",
"serverUrl": "URL เซิร์ฟเวอร์",
"privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว",
"local": "ในเครื่อง",
"server": "เซิร์ฟเวอร์",
"username": "ชื่อผู้ใช้",
"password": "รหัสผ่าน",
"placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์",
"placeholderUsername": "ป้อนชื่อผู้ใช้",
"placeholderPassword": "ป้อนรหัสผ่าน",
"connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ",
"connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS",
"syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ",
"syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS",
"sync": "ซิงค์",
"importSetting": "การตั้งค่าการนำเข้า",
"profile": "โปรไฟล์",
"default": "ค่าเริ่มต้น",
"copyProfiles": "คัดลอกโปรไฟล์",
"addNewProfile": "เพิ่มโปรไฟล์ใหม่",
"createNewProfile": "สร้างโปรไฟล์ใหม่",
"editProfile": "แก้ไขโปรไฟล์",
"placeholderProfileName": "ป้อนชื่อโปรไฟล์",
"profileName": "ชื่อโปรไฟล์",
"create": "สร้าง",
"update": "อัปเดต",
"characterInformation": "ข้อมูลตัวละคร",
"skills": "สกิล",
"showcaseCard": "การ์ดแสดง",
"comingSoon": "เร็วๆ นี้",
"characterName": "ชื่อตัวละคร",
"placeholderCharacter": "ป้อนชื่อตัวละคร",
"characterSettings": "การตั้งค่าตัวละคร",
"levelConfiguration": "การกำหนดระดับเลเวล",
"characterLevel": "เลเวลตัวละคร",
"max": "สูงสุด",
"ultimateEnergy": "พลังงานท่าไม้ตาย",
"currentEnergy": "พลังงานปัจจุบัน",
"setTo50": "ตั้งค่าเป็น 50%",
"battleConfiguration": "การกำหนดค่าการต่อสู้",
"useTechnique": "ใช้เทคนิค",
"techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้",
"enhancement": "การเสริมพลัง",
"enhancementLevel": "ระดับการเสริมพลัง",
"origin": "ดั้งเดิม",
"enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ",
"lightconeEquipment": "สวมใส่ Light Cone",
"lightconeSettings": "การตั้งค่า Light Cone",
"placeholderLevel": "ป้อนเลเวล",
"superimpositionRank": "ระดับการขัดเกลา",
"ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า",
"changeLightcone": "เปลี่ยน Light Cone",
"removeLightcone": "ถอด Light Cone",
"equipLightcone": "สวมใส่ Light Cone",
"noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone",
"equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง",
"filter": "กรอง",
"selectedCharacters": "ตัวละครที่เลือก",
"selectedProfiles": "โปรไฟล์ที่เลือก",
"clearAll": "ล้างทั้งหมด",
"selectAll": "เลือกทั้งหมด",
"copy": "คัดลอก",
"copied": "คัดลอกแล้ว",
"noAvatarSelected": "ไม่ได้เลือกตัวละคร",
"noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก",
"pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์",
"pleaseEnterUid": "โปรดป้อน UID",
"failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้",
"pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร",
"noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า",
"pleaseSelectAFile": "โปรดเลือกไฟล์",
"fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง",
"importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ",
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
"getData": "รับข้อมูล",
"import": "นำเข้า",
"freeSRImport": "นำเข้า FreeSR",
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
"pickAFile": "เลือกไฟล์",
"lightConeSetting": "การตั้งค่า Light Cone",
"relicMaker": "สร้างรีลิกส์",
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
"relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ",
"mainSettings": "การตั้งค่าหลัก",
"mainStat": "ค่าสเตตัสหลัก",
"set": "เซ็ต",
"pleaseSelectASet": "โปรดเลือกเซ็ต",
"effectBonus": "โบนัสเอฟเฟกต์",
"totalRoll": "จำนวนการอัปเกรด",
"randomizeStats": "สุ่มสเตตัส",
"randomizeRolls": "สุ่มอัปเกรด",
"selectASubStat": "เลือกสเตตัสรอง",
"selectASet": "เลือกเซ็ต",
"selectAMainStat": "เลือกสเตตัสหลัก",
"save": "บันทึก",
"reset": "รีเซ็ต",
"roll": "อัปเกรด",
"step": "ขั้น",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "ศัตรูกำหนดเอง",
"simulatedUniverse": "Simulated Universe",
"floor": "ชั้น",
"side": "ฝั่ง",
"wave": "เวฟ",
"stage": "ด่าน",
"useCycleCount": "ใช้การนับรอบหรือไม่?",
"useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?",
"firstHalfEnemies": "ศัตรูครึ่งแรก",
"secondHalfEnemies": "ศัตรูครึ่งหลัง",
"firstNodeEnemies": "ศัตรูโหนด 1",
"secondNodeEnemies": "ศัตรูโหนด 2",
"thirdNodeEnemies": "ศัตรูโหนด 3",
"firstNode": "โหนด 1",
"secondNode": "โหนด 2",
"thirdNode": "โหนด 3",
"listEnemies": "รายการศัตรู",
"turbulenceBuff": "บัฟบรรยากาศ",
"noEventSelected": "ไม่ได้เลือกอีเวนต์",
"noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ",
"upper": "บน",
"lower": "ล่าง",
"upperToLower": "บน -> ล่าง",
"lowerToUpper": "ล่าง -> บน",
"selectMOCEvent": "เลือกอีเวนต์ MOC",
"selectPFEvent": "เลือกอีเวนต์ PF",
"selectASEvent": "เลือกอีเวนต์ AS",
"selectCEEvent": "เลือกอีเวนต์ CE",
"selectEvent": "เลือกอีเวนต์",
"selectFloor": "เลือกชั้น",
"selectSide": "เลือกฝั่ง",
"selectBuff": "เลือกบัฟ",
"selectStage": "เลือกด่าน",
"previous": "ก่อนหน้า",
"next": "ถัดไป",
"noMonstersFound": "ไม่พบมอนสเตอร์",
"addNewWave": "เพิ่มเวฟใหม่",
"searchStage": "ค้นหาด่าน...",
"noStageFound": "ไม่พบด่าน",
"searchMonster": "ค้นหามอนสเตอร์...",
"changeRelic": "เปลี่ยนรีลิกส์",
"deleteRelic": "ลบรีลิกส์",
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
"setEffects": "ตั้งค่าเอฟเฟกต์",
"details": "รายละเอียด",
"normal": "โจมตีปกติ",
"bpskill": "สกิลต่อสู้",
"maze": "เทคนิค",
"ultra": "ท่าไม้ตาย",
"assist": "สกิลช่วยเหลือ",
"servantskill": "สกิล Memosprite",
"severaltalent": "พรสวรรค์ Memosprite",
"singleattack": "โจมตีเป้าหมายเดียว",
"enhance": "เสริมพลัง",
"summon": "อัญเชิญ",
"mazeattack": "โจมตีแบบเทคนิค",
"blast": "กระจาย",
"restore": "ฟื้นฟู",
"support": "สนับสนุน",
"aoeattack": "โจมตีหมู่",
"impair": "ป่วน",
"bounce": "ชิ่ง",
"active": "เปิดใช้งาน",
"defence": "ป้องกัน",
"inactive": "ปิดใช้งาน",
"maxAll": "อัปเกรดทั้งหมด",
"maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ",
"maxAllFailed": "อัปเกรดสกิลล้มเหลว",
"noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "โหมดปกติ",
"hardMode": "โหมดยาก",
"selectPEAKEvent": "เลือกอีเวนต์ PEAK",
"mode": "โหมด",
"selectMode": "เลือกโหมด",
"rollBack": "ย้อนกลับ",
"upRoll": "เพิ่มอัปเกรด",
"downRoll": "ลดอัปเกรด",
"actions": "แอ็กชัน",
"avatars": "อวตาร",
"quickView": "ดูแบบด่วน",
"extraSetting": "การตั้งค่าเพิ่มเติม",
"disableCensorship": "ปิดการเซ็นเซอร์",
"hideUI": "ซ่อน UI",
"theoryCraftMode": "โหมด Theorycraft",
"cycleCount": "จำนวนรอบ",
"pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด",
"subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์",
"theoryCraft": "Theorycraft",
"multipathCharacter": "ตัวละครหลาย Path",
"mainPath": "Path หลัก",
"march7Path": "Path March 7th",
"challenge": "ท้าทาย",
"skipNode": "ข้ามโหนด",
"disableSkip": "ปิดใช้งานการข้าม",
"skipNode1": "ข้ามโหนด 1",
"skipNode2": "ข้ามโหนด 2",
"extraFeatures": "คุณสมบัติพิเศษ",
"detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้",
"detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction",
"detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน",
"detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม",
"detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม",
"detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว",
"trailblazer": "ผู้บุกเบิก",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"extra": "พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง"
}
"severaltalent": "พรสวรรค์ Memosprite",
"singleattack": "โจมตีเป้าหมายเดียว",
"enhance": "เสริมพลัง",
"summon": "อัญเชิญ",
"mazeattack": "โจมตีแบบเทคนิค",
"blast": "กระจาย",
"restore": "ฟื้นฟู",
"support": "สนับสนุน",
"aoeattack": "โจมตีหมู่",
"impair": "ป่วน",
"bounce": "ชิ่ง",
"active": "เปิดใช้งาน",
"defence": "ป้องกัน",
"inactive": "ปิดใช้งาน",
"maxAll": "อัปเกรดทั้งหมด",
"maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ",
"maxAllFailed": "อัปเกรดสกิลล้มเหลว",
"noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "โหมดปกติ",
"hardMode": "โหมดยาก",
"selectPEAKEvent": "เลือกอีเวนต์ PEAK",
"mode": "โหมด",
"selectMode": "เลือกโหมด",
"rollBack": "ย้อนกลับ",
"upRoll": "เพิ่มอัปเกรด",
"downRoll": "ลดอัปเกรด",
"actions": "แอ็กชัน",
"avatars": "อวตาร",
"quickView": "ดูแบบด่วน",
"extraSetting": "การตั้งค่าเพิ่มเติม",
"disableCensorship": "ปิดการเซ็นเซอร์",
"hideUI": "ซ่อน UI",
"theoryCraftMode": "โหมด Theorycraft",
"cycleCount": "จำนวนรอบ",
"pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด",
"subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์",
"theoryCraft": "Theorycraft",
"multipathCharacter": "ตัวละครหลาย Path",
"mainPath": "Path หลัก",
"march7Path": "Path March 7th",
"challenge": "ท้าทาย",
"skipNode": "ข้ามโหนด",
"disableSkip": "ปิดใช้งานการข้าม",
"skipNode1": "ข้ามโหนด 1",
"skipNode2": "ข้ามโหนด 2",
"extraFeatures": "คุณสมบัติพิเศษ",
"detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้",
"detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction",
"detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน",
"detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม",
"detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม",
"detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว",
"trailblazer": "ผู้บุกเบิก",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"extra": "พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง",
"def": "DEF",
"language": "ภาษา"
}
}
+286 -284
View File
@@ -1,290 +1,292 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng",
"character": "Nhân vật",
"id": "ID",
"path": "Vận mệnh",
"rarity": "Độ hiếm",
"element": "Nguyên tố",
"technique": "Bí kỹ",
"talent": "Thiên phú",
"basic": "Đánh thường",
"skill": "Kỹ năng",
"ultimate": "Tuyệt kỹ",
"servant": "Phụ trợ",
"damage": "Sát thương",
"type": "Loại",
"warrior": "Hủy Diệt",
"knight": "Bảo Hộ",
"mage": "Tri Thức",
"priest": "Trù phú",
"rogue": "Săn Bắn",
"shaman": "Hòa Hợp",
"warlock": "Hư Vô",
"memory": "Ký Ức",
"elation": "Vui vẻ",
"fire": "Hỏa",
"ice": "Băng",
"imaginary": "Ảo Ảnh",
"physical": "Vật Lý",
"quantum": "Lượng Tử",
"thunder": "Lôi",
"wind": "Phong",
"hp": "HP",
"atk": "Tấn công",
"speed": "Tốc độ",
"critRate": "Tỷ lệ bạo",
"critDmg": "ST bạo",
"breakEffect": "sát thương kích phá",
"effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Tốc độ hồi năng lượng",
"effectHitRate": "Tỷ lệ trúng hiệu ứng",
"outgoingHealingBoost": "Tăng hồi phục",
"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 Ảo Ảnh",
"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 thêm",
"true damage": "Sát thương chuẩn",
"elationdamage": "Sát thương vui vẻ",
"follow-up": "Sát thương phản kích",
"elemental damage": "Sát thương kích phá và siêu kích phá",
"dot": "Sát thương theo thời gian",
"qte": "Kỹ năng QTE",
"level": "Cấp độ",
"relics": "Thánh di vật",
"eidolons": "Tinh hồn",
"lightcones": "Nón ánh sáng",
"loadData": "Tải dữ liệu",
"exportData": "Xuất dữ liệu",
"connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối",
"unconnected": "Chưa kết nối",
"psConnection": "Kết nối PS",
"connectionType": "Loại kết nối",
"status": "Trạng thái",
"connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối",
"other": "Khác",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Cài đặt quái",
"serverUrl": "Địa chỉ server",
"privateType": "Loại riêng tư",
"local": "Cục bộ",
"server": "Máy chủ",
"username": "Tên người dùng",
"password": "Mật khẩu",
"placeholderServerUrl": "Nhập địa chỉ server",
"placeholderUsername": "Nhập tên người dùng",
"placeholderPassword": "Nhập mật khẩu",
"connectedSuccess": "Kết nối PS thành công",
"connectedFailed": "Kết nối PS thất bại",
"syncSuccess": "Đồng bộ dữ liệu với PS thành công",
"syncFailed": "Đồng bộ dữ liệu với PS thất bại",
"sync": "Đồng bộ",
"importSetting": "Cài đặt nhập",
"profile": "Hồ sơ",
"default": "Mặc định",
"copyProfiles": "Sao chép hồ sơ",
"addNewProfile": "Thêm hồ sơ mới",
"createNewProfile": "Tạo hồ sơ mới",
"editProfile": "Chỉnh sửa hồ sơ",
"placeholderProfileName": "Nhập tên hồ sơ",
"profileName": "Tên hồ sơ",
"create": "Tạo",
"update": "Cập nhật",
"characterInformation": "Thông tin nhân vật",
"skills": "Kỹ năng",
"showcaseCard": "Thẻ trưng bày",
"comingSoon": "Sắp ra mắt",
"characterName": "Tên nhân vật",
"placeholderCharacter": "Nhập tên nhân vật",
"characterSettings": "Cài đặt nhân vật",
"levelConfiguration": "Cấu hình cấp độ",
"characterLevel": "Cấp độ nhân vật",
"max": "Tối đa",
"ultimateEnergy": "Năng lượng tuyệt kỹ",
"currentEnergy": "Năng lượng hiện tại",
"setTo50": "Đặt thành 50%",
"battleConfiguration": "Cấu hình trận đấu",
"useTechnique": "Dùng bí kỹ",
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
"enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa",
"origin": "Nguyên gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng",
"placeholderLevel": "Nhập cấp độ",
"superimpositionRank": "Bậc chồng kỹ năng",
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
"changeLightcone": "Thay đổi nón ánh sáng",
"removeLightcone": "Gỡ nón ánh sáng",
"equipLightcone": "Trang bị nón ánh sáng",
"noLightconeEquipped": "Chưa trang bị nón ánh sáng",
"equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
"filter": "Lọc",
"selectedCharacters": "Nhân vật đã chọn",
"selectedProfiles": "Hồ sơ đã chọn",
"clearAll": "Xóa tất cả",
"selectAll": "Chọn tất cả",
"copy": "Sao chép",
"copied": "Đã sao chép",
"noAvatarSelected": "Chưa chọn nhân vật",
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép",
"pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
"pleaseEnterUid": "Vui lòng nhập UID",
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại",
"pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
"noDataToImport": "Không có dữ liệu để nhập",
"pleaseSelectAFile": "Vui lòng chọn một tệp",
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
"getData": "Lấy dữ liệu",
"import": "Nhập",
"freeSRImport": "Nhập FreeSR",
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
"pickAFile": "Chọn tệp",
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
"relicMaker": "Trình tạo Thánh Di Vật",
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
"relicSavedSuccessfully": "Lưu thánh di vật thành công",
"mainSettings": "Cài đặt chính",
"mainStat": "Chỉ số chính",
"set": "Bộ",
"pleaseSelectASet": "Vui lòng chọn một bộ",
"effectBonus": "Hiệu ứng cộng thêm",
"totalRoll": "Tổng số dòng",
"randomizeStats": "Ngẫu nhiên chỉ số",
"randomizeRolls": "Ngẫu nhiên số dòng",
"selectASubStat": "Chọn chỉ số phụ",
"selectASet": "Chọn một bộ",
"selectAMainStat": "Chọn chỉ số chính",
"save": "Lưu",
"reset": "Đặt lại toàn bộ",
"roll": "Số dòng",
"step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng",
"side": "Nửa trận",
"wave": "Đợt",
"stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau",
"firstNodeEnemies": "Địch Node 1",
"secondNodeEnemies": "Địch Node 2",
"thirdNodeEnemies": "Địch Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên",
"lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE",
"selectPEAKEvent": "Chọn sự kiện PEAK",
"selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff",
"selectStage": "Chọn màn",
"previous": "Trước",
"next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết",
"normal": "Đánh thường",
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng",
"character": "Nhân vật",
"id": "ID",
"path": "Vận mệnh",
"rarity": "Độ hiếm",
"element": "Nguyên tố",
"technique": "Bí kỹ",
"talent": "Thiên phú",
"basic": "Đánh thường",
"skill": "Kỹ năng",
"ultimate": "Tuyệt kỹ",
"servant": "Phụ trợ",
"damage": "Sát thương",
"type": "Loại",
"warrior": "Hủy Diệt",
"knight": "Bảo Hộ",
"mage": "Tri Thức",
"priest": "Trù phú",
"rogue": "Săn Bắn",
"shaman": "Hòa Hợp",
"warlock": "Hư Vô",
"memory": "Ký Ức",
"elation": "Vui vẻ",
"fire": "Hỏa",
"ice": "Băng",
"imaginary": "Ảo Ảnh",
"physical": "Vật Lý",
"quantum": "Lượng Tử",
"thunder": "Lôi",
"wind": "Phong",
"hp": "HP",
"atk": "Tấn công",
"speed": "Tốc độ",
"critRate": "Tỷ lệ bạo",
"critDmg": "ST bạo",
"breakEffect": "sát thương kích phá",
"effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Tốc độ hồi năng lượng",
"effectHitRate": "Tỷ lệ trúng hiệu ứng",
"outgoingHealingBoost": "Tăng hồi phục",
"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 Ảo Ảnh",
"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 thêm",
"true damage": "Sát thương chuẩn",
"elationdamage": "Sát thương vui vẻ",
"follow-up": "Sát thương phản kích",
"elemental damage": "Sát thương kích phá và siêu kích phá",
"dot": "Sát thương theo thời gian",
"qte": "Kỹ năng QTE",
"level": "Cấp độ",
"relics": "Thánh di vật",
"eidolons": "Tinh hồn",
"lightcones": "Nón ánh sáng",
"loadData": "Tải dữ liệu",
"exportData": "Xuất dữ liệu",
"connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối",
"unconnected": "Chưa kết nối",
"psConnection": "Kết nối PS",
"connectionType": "Loại kết nối",
"status": "Trạng thái",
"connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối",
"other": "Khác",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Cài đặt quái",
"serverUrl": "Địa chỉ server",
"privateType": "Loại riêng tư",
"local": "Cục bộ",
"server": "Máy chủ",
"username": "Tên người dùng",
"password": "Mật khẩu",
"placeholderServerUrl": "Nhập địa chỉ server",
"placeholderUsername": "Nhập tên người dùng",
"placeholderPassword": "Nhập mật khẩu",
"connectedSuccess": "Kết nối PS thành công",
"connectedFailed": "Kết nối PS thất bại",
"syncSuccess": "Đồng bộ dữ liệu với PS thành công",
"syncFailed": "Đồng bộ dữ liệu với PS thất bại",
"sync": "Đồng bộ",
"importSetting": "Cài đặt nhập",
"profile": "Hồ sơ",
"default": "Mặc định",
"copyProfiles": "Sao chép hồ sơ",
"addNewProfile": "Thêm hồ sơ mới",
"createNewProfile": "Tạo hồ sơ mới",
"editProfile": "Chỉnh sửa hồ sơ",
"placeholderProfileName": "Nhập tên hồ sơ",
"profileName": "Tên hồ sơ",
"create": "Tạo",
"update": "Cập nhật",
"characterInformation": "Thông tin nhân vật",
"skills": "Kỹ năng",
"showcaseCard": "Thẻ trưng bày",
"comingSoon": "Sắp ra mắt",
"characterName": "Tên nhân vật",
"placeholderCharacter": "Nhập tên nhân vật",
"characterSettings": "Cài đặt nhân vật",
"levelConfiguration": "Cấu hình cấp độ",
"characterLevel": "Cấp độ nhân vật",
"max": "Tối đa",
"ultimateEnergy": "Năng lượng tuyệt kỹ",
"currentEnergy": "Năng lượng hiện tại",
"setTo50": "Đặt thành 50%",
"battleConfiguration": "Cấu hình trận đấu",
"useTechnique": "Dùng bí kỹ",
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
"enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa",
"origin": "Nguyên gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng",
"placeholderLevel": "Nhập cấp độ",
"superimpositionRank": "Tích tầng",
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
"changeLightcone": "Thay đổi nón ánh sáng",
"removeLightcone": "Gỡ nón ánh sáng",
"equipLightcone": "Trang bị nón ánh sáng",
"noLightconeEquipped": "Chưa trang bị nón ánh sáng",
"equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
"filter": "Lọc",
"selectedCharacters": "Nhân vật đã chọn",
"selectedProfiles": "Hồ sơ đã chọn",
"clearAll": "Xóa tất cả",
"selectAll": "Chọn tất cả",
"copy": "Sao chép",
"copied": "Đã sao chép",
"noAvatarSelected": "Chưa chọn nhân vật",
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép",
"pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
"pleaseEnterUid": "Vui lòng nhập UID",
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại",
"pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
"noDataToImport": "Không có dữ liệu để nhập",
"pleaseSelectAFile": "Vui lòng chọn một tệp",
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
"getData": "Lấy dữ liệu",
"import": "Nhập",
"freeSRImport": "Nhập FreeSR",
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
"pickAFile": "Chọn tệp",
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
"relicMaker": "Trình tạo Thánh Di Vật",
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
"relicSavedSuccessfully": "Lưu thánh di vật thành công",
"mainSettings": "Cài đặt chính",
"mainStat": "Chỉ số chính",
"set": "Bộ",
"pleaseSelectASet": "Vui lòng chọn một bộ",
"effectBonus": "Hiệu ứng cộng thêm",
"totalRoll": "Tổng số dòng",
"randomizeStats": "Ngẫu nhiên chỉ số",
"randomizeRolls": "Ngẫu nhiên số dòng",
"selectASubStat": "Chọn chỉ số phụ",
"selectASet": "Chọn một bộ",
"selectAMainStat": "Chọn chỉ số chính",
"save": "Lưu",
"reset": "Đặt lại toàn bộ",
"roll": "Số dòng",
"step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng",
"side": "Nửa trận",
"wave": "Đợt",
"stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau",
"firstNodeEnemies": "Địch Node 1",
"secondNodeEnemies": "Địch Node 2",
"thirdNodeEnemies": "Địch Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên",
"lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE",
"selectPEAKEvent": "Chọn sự kiện PEAK",
"selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff",
"selectStage": "Chọn màn",
"previous": "Trước",
"next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết",
"normal": "Đánh thường",
"bpskill": "Chiến kỹ",
"maze": "Bí kỹ",
"ultra": "Tuyệt kỹ",
"assist": "Kỹ năng hỗ trợ",
"servantskill": "Kỹ năng vật triệu hồi",
"severaltalent": "Thiên phú vật triệu hồi",
"singleattack": "Tấn công đơn",
"enhance": "Cường hóa",
"summon": "Triệu hồi",
"blast": "Tấn công 3 mục tiêu",
"restore": "Hồi phục",
"support": "Hỗ trợ",
"aoeattack": "Tấn công đa mục tiêu",
"mazeattack": "Bí kỹ tấn công",
"impair": "Suy yếu",
"bounce": "Nảy bật",
"active": "Kích hoạt",
"inactive": "Không kích hoạt",
"defence": "Phòng thủ",
"maxAll": "Tối đa tất cả",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
"noRelicEquipped": "Không có di vật",
"anomalyArbitration": "Trọng tài dị tướng",
"normalMode": "Chế độ thường",
"hardMode": "Chế độ khó",
"mode": "Chế độ",
"selectMode": "Chọn chế độ",
"rollBack": "Quay lại bước trước",
"upRoll": "Tăng dòng",
"downRoll": "Giảm dòng",
"actions": "Hành động",
"avatars": "Nhân vật",
"quickView": "Xem nhanh",
"extraSetting": "Cài đặt bổ sung",
"disableCensorship": "Tắt kiểm duyệt",
"hideUI": "Ẩn giao diện",
"theoryCraftMode": "Chế độ Theory Craft",
"cycleCount": "Số vòng",
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
"theoryCraft": "Theory Craft",
"multipathCharacter": "Nhân vật đa Vận Mệnh",
"mainPath": "Vận Mệnh Nhân Vật Chính",
"march7Path": "Vận Mệnh March 7",
"challenge": "Thử thách",
"skipNode": "Bỏ qua node",
"disableSkip": "Tắt bỏ qua",
"skipNode1": "Bỏ qua node 1",
"skipNode2": "Bỏ qua node 2",
"extraFeatures": "Tính năng bổ sung",
"detailTheoryCraft": "Khi bật tính năng này sẽ cho phép tùy chỉnh số cycle và trong mục kẻ địch tủy chỉnh sẽ cho phép điều chỉnh số hp.",
"detailSkipNode": "Khi bật tính năng này sẽ cho phép bỏ qua (node 1/node 2) của Hồi ức hỗn độn hoặc Kể chuyện hư cấu.",
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
"trailblazer": "Nhà khai phá",
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
"extra": "Bổ sung",
"customLineup": "Đội hình tùy chỉnh"
}
"severaltalent": "Thiên phú vật triệu hồi",
"singleattack": "Tấn công đơn",
"enhance": "Cường hóa",
"summon": "Triệu hồi",
"blast": "Tấn công 3 mục tiêu",
"restore": "Hồi phục",
"support": "Hỗ trợ",
"aoeattack": "Tấn công đa mục tiêu",
"mazeattack": "Bí kỹ tấn công",
"impair": "Suy yếu",
"bounce": "Nảy bật",
"active": "Kích hoạt",
"inactive": "Không kích hoạt",
"defence": "Phòng thủ",
"maxAll": "Tối đa tất cả",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
"noRelicEquipped": "Không có di vật",
"anomalyArbitration": "Trọng tài dị tướng",
"normalMode": "Chế độ thường",
"hardMode": "Chế độ khó",
"mode": "Chế độ",
"selectMode": "Chọn chế độ",
"rollBack": "Quay lại bước trước",
"upRoll": "Tăng dòng",
"downRoll": "Giảm dòng",
"actions": "Hành động",
"avatars": "Nhân vật",
"quickView": "Xem nhanh",
"extraSetting": "Cài đặt bổ sung",
"disableCensorship": "Tắt kiểm duyệt",
"hideUI": "Ẩn giao diện",
"theoryCraftMode": "Chế độ Theory Craft",
"cycleCount": "Số vòng",
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
"theoryCraft": "Theory Craft",
"multipathCharacter": "Nhân vật đa Vận Mệnh",
"mainPath": "Vận Mệnh Nhân Vật Chính",
"march7Path": "Vận Mệnh March 7",
"challenge": "Thử thách",
"skipNode": "Bỏ qua node",
"disableSkip": "Tắt bỏ qua",
"skipNode1": "Bỏ qua node 1",
"skipNode2": "Bỏ qua node 2",
"extraFeatures": "Tính năng bổ sung",
"detailTheoryCraft": "Khi bật tính năng này sẽ cho phép tùy chỉnh số cycle và trong mục kẻ địch tủy chỉnh sẽ cho phép điều chỉnh số hp.",
"detailSkipNode": "Khi bật tính năng này sẽ cho phép bỏ qua (node 1/node 2) của Hồi ức hỗn độn hoặc Kể chuyện hư cấu.",
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
"trailblazer": "Nhà khai phá",
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
"extra": "Bổ sung",
"customLineup": "Đội hình tùy chỉnh",
"def": "Phòng thủ",
"language": "Ngôn ngữ"
}
}
+4 -2
View File
@@ -285,6 +285,8 @@
"trailblazer": "开拓者",
"listExtraEffect": "额外效果列表",
"extra": "额外",
"customLineup": "自定义阵容"
"customLineup": "自定义阵容",
"def": "防御力",
"language": "语言"
}
}
}
+12 -12
View File
@@ -10,15 +10,15 @@
},
"dependencies": {
"@next/bundle-analyzer": "16.1.6",
"@tanstack/react-query": "^5.100.10",
"axios": "^1.16.1",
"@tanstack/react-query": "^5.102.5",
"axios": "^1.20.0",
"fast-average-color": "^9.5.2",
"framer-motion": "^12.38.0",
"framer-motion": "^12.43.0",
"html2canvas-pro": "^1.6.7",
"lru-cache": "^11.3.6",
"lru-cache": "^11.5.2",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-intl": "^4.12.0",
"next-intl": "^4.13.7",
"prismjs": "^1.30.0",
"react": "19.2.4",
"react-dom": "19.2.4",
@@ -27,20 +27,20 @@
"react-toastify": "^11.1.0",
"sharp": "^0.34.5",
"zod": "^4.4.3",
"zustand": "^5.0.13"
"zustand": "^5.0.15"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
"@tailwindcss/postcss": "^4.3.3",
"@types/jest": "^30.0.0",
"@types/node": "^25.8.0",
"@types/node": "^25.9.5",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"baseline-browser-mapping": "^2.10.29",
"daisyui": "^5.5.19",
"eslint": "^9.39.4",
"baseline-browser-mapping": "^2.11.19",
"daisyui": "^5.7.22",
"eslint": "^9.39.5",
"eslint-config-next": "16.1.6",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.0",
"tailwindcss": "^4.3.3",
"ts-to-zod": "^5.1.0",
"typescript": "^5.9.3"
},
+125
View File
@@ -0,0 +1,125 @@
{
"version": 1,
"skills": {
"banner-design": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/banner-design/SKILL.md",
"computedHash": "f859a951ded1109a38bcc7f18faf4581f9adbf0550df7a11c013e77a5b229541"
},
"brand": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/brand/SKILL.md",
"computedHash": "0e773774d46ecea78eff2b36f574e41830417fcc618181215611f336930deac2"
},
"brandkit": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/brandkit/SKILL.md",
"computedHash": "735007879f5110aeae1e981a53fe74a12afde70347f9773dacd50e4697a091bf"
},
"design": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/design/SKILL.md",
"computedHash": "bb2e3ffee4b7b69e5a38ce08f6010ca79381d9f53b9dfadb33aee1ece6f1679a"
},
"design-system": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/design-system/SKILL.md",
"computedHash": "5a3dbae4a3c2ad9f96323d9bfae926b3c21bb1e3b066aa067906f577a2ff2545"
},
"design-taste-frontend": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/taste-skill/SKILL.md",
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
},
"design-taste-frontend-v1": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/taste-skill-v1/SKILL.md",
"computedHash": "0a226c13d69639553cf8a7c40aa5527b026820f8ddaf7a6b263988e32ab44a27"
},
"full-output-enforcement": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/output-skill/SKILL.md",
"computedHash": "524f7c7950c3fde726101613f630140411c7d080e63be718687f4b38f919be81"
},
"gpt-taste": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/gpt-tasteskill/SKILL.md",
"computedHash": "7bd1bfdda0e51016ca222693401a65fc0dd9bf9487e0bd5b1740ba510f6f963a"
},
"high-end-visual-design": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/soft-skill/SKILL.md",
"computedHash": "f730e4132775f13eea19e3dc39afc6bb453cfc0498872b127ad8f0d47cfd802d"
},
"image-to-code": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/image-to-code-skill/SKILL.md",
"computedHash": "3d2ac14d956fdafc5500d2108b90895e134d847f9f199e7f1d18bf1f814cf7c8"
},
"imagegen-frontend-mobile": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/imagegen-frontend-mobile/SKILL.md",
"computedHash": "7c1d1dc59cd0df4063b8621af543bf5dea1d576b261507e17b0323c69680b048"
},
"imagegen-frontend-web": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/imagegen-frontend-web/SKILL.md",
"computedHash": "27255c7c50181e58add156b2aa2bd5325285b85f04027e4c8b73742d98d7be0a"
},
"industrial-brutalist-ui": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/brutalist-skill/SKILL.md",
"computedHash": "e66e5a2a8b73cef79ab6546459c665e8b983645689c5f3772f7eed8b9f1e8e0e"
},
"minimalist-ui": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/minimalist-skill/SKILL.md",
"computedHash": "7e88897e0371184e97a82b8d9ef080323c13f21eb067b58ef71eb05173a08ac2"
},
"redesign-existing-projects": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/redesign-skill/SKILL.md",
"computedHash": "47eced3e960a2c961d3cbee11fa1216264f589344b586a5cbd03967cd6b9c54b"
},
"slides": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/slides/SKILL.md",
"computedHash": "a2b3559b58afc484548e1eb359e20495d11cf12b90da7c1fbba50efed9a70340"
},
"stitch-design-taste": {
"source": "Leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/stitch-skill/SKILL.md",
"computedHash": "c96b580192b34167efa01f460bbc669ff4315cf97f3cf6ff85ded8029aa720b8"
},
"ui-styling": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/ui-styling/SKILL.md",
"computedHash": "e10b225db04fdfd893fdda2b9ed893c315f9dee5693494743d57b02066e96a51"
},
"ui-ux-pro-max": {
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"sourceType": "github",
"skillPath": ".claude/skills/ui-ux-pro-max/SKILL.md",
"computedHash": "d1de8ff8d228cd4204bf06eb959ef4317d5b927908d9f42765971e33e80c8a7e"
}
}
}
+24 -24
View File
@@ -1,25 +1,25 @@
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
}
+70 -70
View File
@@ -1,79 +1,79 @@
import { NextRequest, NextResponse } from 'next/server'
import axios from 'axios'
import net from 'net'
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
let response
switch (method.toUpperCase()) {
+9 -9
View File
@@ -1,9 +1,9 @@
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
+80 -8
View File
@@ -1,6 +1,5 @@
@import "tailwindcss";
@plugin "daisyui" {
exclude: properties;
themes: winter --default, night --prefersdark, cupcake, coffee;
@@ -9,19 +8,19 @@
@tailwind utilities;
@plugin 'tailwind-scrollbar' {
nocompatible: true;
nocompatible: true;
}
:root {
--size-big: 4vw;
--size-medium: 3vw;
--size-small: 2vw;
}
/* Smooth Modern Ultra-Sleek Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
@@ -29,10 +28,83 @@
}
::-webkit-scrollbar-thumb {
background-color: #9ca3af;
border-radius: 0;
background: rgba(156, 163, 175, 0.28);
border-radius: 9999px;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.55);
}
::-webkit-scrollbar-button {
display: none;
display: none;
}
/* Custom UI Utilities for Gaming Companion Aesthetic */
@layer utilities {
.glass-card {
background-color: var(--color-base-100);
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
}
.glass-panel {
background-color: var(--color-base-100);
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
}
.hover-lift {
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
}
.active-press {
transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.active-press:active {
transform: scale(0.97);
}
/* Rarity Glow Effects */
.glow-5star {
box-shadow: 0 0 16px -2px rgba(245, 158, 11, 0.35);
}
.glow-4star {
box-shadow: 0 0 16px -2px rgba(168, 85, 247, 0.35);
}
/* Ambient Background - Clean Canvas */
.bg-ambient-mesh {
background-color: var(--color-base-100);
}
/* Theme Adaptive Path & Stat Icon Filters - High Accessibility Contrast */
.theme-path-icon {
filter: brightness(0.2) opacity(0.85);
}
[data-theme="night"] .theme-path-icon,
[data-theme="coffee"] .theme-path-icon {
filter: brightness(0.8) opacity(0.85);
}
.theme-path-icon-active {
filter: brightness(0);
}
[data-theme="night"] .theme-path-icon-active,
[data-theme="coffee"] .theme-path-icon-active {
filter: brightness(1);
}
/* Stat icons (HSR raw sprite icons are white pngs, need dark filter on light themes) */
.theme-stat-icon {
filter: brightness(0.15) opacity(0.8);
}
[data-theme="night"] .theme-stat-icon,
[data-theme="coffee"] .theme-stat-icon {
filter: brightness(0.95) opacity(1);
}
}
+12 -10
View File
@@ -64,9 +64,9 @@ export default async function RootLayout({
const locale = await getLocale()
return (
<html lang={locale} suppressHydrationWarning>
<html lang={locale} suppressHydrationWarning className="overflow-x-hidden">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden max-w-full`}
suppressHydrationWarning
>
<NextIntlClientProvider messages={messages}>
@@ -74,17 +74,19 @@ export default async function RootLayout({
<ThemeProvider>
<ClientThemeWrapper>
<ClientDataFetcher >
<div className="min-h-screen w-full">
<div className="min-h-screen w-full flex flex-col justify-between bg-ambient-mesh selection:bg-primary/20 selection:text-primary">
<Header />
<div className="grid grid-cols-12 w-full">
<div className="hidden sm:block md:col-span-4 lg:col-span-3 sticky top-0 self-start h-fit">
<main className="grid grid-cols-12 w-full flex-1">
<aside className="hidden sm:block sm:col-span-4 md:col-span-4 lg:col-span-3 sticky top-16 self-start h-[calc(100vh-4rem)] overflow-hidden border-r border-base-content/10 bg-base-100/30 backdrop-blur-sm">
<AvatarBar />
</div>
<div className="col-span-12 sm:col-span-8 lg:col-span-9">
</aside>
<section className="col-span-12 sm:col-span-8 md:col-span-8 lg:col-span-9 flex flex-col min-w-0">
<ActionBar />
{children}
</div>
</div>
<div className="flex-1 p-2 sm:p-3 sm:pt-1">
{children}
</div>
</section>
</main>
<Footer />
</div>
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
+509 -444
View File
@@ -1,445 +1,510 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function ActionBar() {
const router = useRouter()
const { avatarSelected } = useCurrentDataStore()
const { damageType, baseType } = useDetailDataStore()
const { setResetData } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore()
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList[avatar.profileSelect] || null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newListProfile = [...listProfile]
const newProfile = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
}
newListProfile.push(newProfile)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = [...listProfile]
newListProfile[profileEdit].profile_name = profileName;
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId == 0) return;
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
const newListProfile = [...listProfile]
newListProfile.splice(profileId, 1)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
}
}
const handleConnectOrSyncPS = async () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars").toUpperCase(),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
return (
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
<div className="flex flex-col justify-center w-full">
<div className="flex flex-wrap items-center gap-2 ">
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
{avatarSelected && (
<div className="flex flex-wrap items-center justify-start h-full w-full">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt={'damage type'}
unoptimized
crossOrigin="anonymous"
className="h-10 w-10 object-contain"
width={100}
height={100}
/>
<p className="text-center font-bold text-lg">
{transI18n(avatarSelected.BaseType.toLowerCase())}
</p>
<div className="text-center font-bold text-lg">{" / "}</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className={"font-bold text-lg"}
/>
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
</div>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-center md:dropdown-start">
<div
tabIndex={0}
role="button"
className="btn btn-warning border-info btn-soft gap-1"
>
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name}
</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
{listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}
{index === 0 && (
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
)}
</span>
</button>
{index !== 0 && (
<>
<button
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
<button
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
onClick={(e) => {
handleDeleteProfile(index, e)
}}
title="Delete Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
</>
)}
</li>
))}
<li className="border-t border-base-300 mt-2 pt-2 z-10">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("copyProfiles")}
</button>
<button
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
<button
onClick={() => {
setIsOpenAvatars(true)
handleShow("avatars_modal")
}}
className="col-span-1 btn btn-warning btn-sm w-full">
{transI18n("avatars")}
</button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
>
<li>
<button onClick={() => actionMove('')}>
{transI18n("characterInformation")}
</button>
</li>
<li>
<button onClick={() => actionMove('relics-info')}>
{transI18n("relics")}
</button>
</li>
<li>
<button onClick={() => actionMove('eidolons-info')}>
{transI18n("eidolons")}
</button>
</li>
<li>
<button onClick={() => actionMove('skills-info')}>
{transI18n("skills")}
</button>
</li>
<li>
<button onClick={() => actionMove('showcase-card')}>
{transI18n("showcaseCard")}
</button>
</li>
<li>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
{transI18n("characterInformation")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
{transI18n("relics")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
{transI18n("eidolons")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
{transI18n("skills")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
{transI18n("showcaseCard")}
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
✕
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
{content}
</div>
</dialog>
))}
</div>
</div>
);
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { usePathname, useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore, AvatarProfileStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
import { User, Shield, Sparkles, Zap, Image as ImageIcon, RefreshCw, Plus, Copy, Edit2, Trash2 } from "lucide-react";
export default function ActionBar() {
const router = useRouter()
const pathname = usePathname()
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
)
const { damageType, baseType } = useDetailDataStore(
useShallow((state) => ({
damageType: state.damageType,
baseType: state.baseType,
}))
)
const { setResetData } = useCopyProfileStore(
useShallow((state) => ({ setResetData: state.setResetData }))
)
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore(
useShallow((state) => ({
isOpenCreateProfile: state.isOpenCreateProfile,
setIsOpenCreateProfile: state.setIsOpenCreateProfile,
isOpenCopy: state.isOpenCopy,
setIsOpenCopy: state.setIsOpenCopy,
isOpenAvatars: state.isOpenAvatars,
setIsOpenAvatars: state.setIsOpenAvatars,
}))
)
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
)
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore(
useShallow((state) => ({ isConnectPS: state.isConnectPS }))
)
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newProfile: AvatarProfileStore = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
};
const newListProfile = [...listProfile, newProfile];
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = listProfile.map((p, idx) =>
idx === profileEdit ? { ...p, profile_name: profileName } : p
);
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected || !avatars[avatarSelected.ID]) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
if (selectedProfile) {
toast.success(`Profile changed to: ${selectedProfile.profile_name}`)
}
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
if (!targetProfile) return;
if (window.confirm(`Are you sure you want to delete profile: ${targetProfile.profile_name}?`)) {
const currentSelect = avatars[avatarSelected.ID].profileSelect;
const newListProfile = listProfile.filter((_, idx) => idx !== profileId);
const nextSelect = currentSelect >= profileId ? Math.max(0, currentSelect - 1) : currentSelect;
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: nextSelect })
toast.success(`Profile ${targetProfile.profile_name} deleted successfully`)
}
}
const handleConnectOrSyncPS = async () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-base-content/80">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-bordered input-primary w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
autoFocus
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
Cancel
</button>
<button className="btn btn-primary" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles"),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars"),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
const navTabs = [
{ label: transI18n("characterInformation"), path: "", icon: User },
{ label: transI18n("relics"), path: "relics-info", icon: Shield },
{ label: transI18n("eidolons"), path: "eidolons-info", icon: Sparkles },
{ label: transI18n("skills"), path: "skills-info", icon: Zap },
{ label: transI18n("showcaseCard"), path: "showcase-card", icon: ImageIcon },
];
const currentActivePath = pathname?.replace(/^\//, '') || "";
return (
<div className="w-full bg-transparent px-3 sm:px-4 py-2 space-y-2">
{/* Top Row: Character Identity + Profile Selector + Connect PS */}
<div className="flex flex-wrap items-center justify-between gap-2.5">
{/* Character Identity */}
{avatarSelected && (
<div className="flex items-center gap-2 h-8.5 px-3 rounded-xl bg-base-200/80 border border-base-content/10 text-xs sm:text-sm font-semibold shadow-xs">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt="damage type"
unoptimized
crossOrigin="anonymous"
className="h-5 w-5 object-contain"
width={20}
height={20}
/>
<span className="text-base-content/85 font-medium">{transI18n(avatarSelected.BaseType.toLowerCase())}</span>
<span className="text-base-content/30">/</span>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className="font-bold text-primary"
/>
<span className="text-[11px] font-mono text-base-content/60 font-semibold">#{avatarSelected.ID}</span>
</div>
)}
{/* Profile Selector & Quick Actions */}
<div className="flex items-center h-8.5 rounded-xl bg-base-200/80 border border-base-content/10 p-0.5 shadow-xs">
<div className="dropdown dropdown-bottom dropdown-start h-full flex items-center">
<div
tabIndex={0}
role="button"
className="btn btn-sm btn-ghost gap-1.5 h-7 min-h-7 font-bold rounded-lg hover:bg-base-300/80 px-2"
>
<span className="text-[11px] uppercase tracking-wider text-base-content/60 font-semibold">{transI18n("profile")}:</span>
<span className="truncate max-w-28 sm:max-w-40 text-xs text-base-content font-bold">
{profileCurrent?.profile_name || "Default Profile"}
</span>
<svg className="w-3.5 h-3.5 opacity-60 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
<ul className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl min-w-60 mt-2 border border-base-300 max-h-72 overflow-y-auto space-y-1">
{listProfile.map((profile, index) => (
<li key={index} className="flex flex-row items-center justify-between rounded-xl px-1 hover:bg-base-200">
<button
className="flex-1 text-left py-2 px-2 text-xs font-medium truncate"
onClick={() => handleProfileSelect(index)}
>
<span className="truncate">{profile.profile_name}</span>
{index === 0 && (
<span className="text-[10px] text-base-content/50 ml-1.5 font-mono">({transI18n("default")})</span>
)}
</button>
{index !== 0 && (
<div className="flex items-center gap-1">
<button
className="btn btn-ghost btn-xs btn-circle text-warning hover:bg-warning/20"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<Edit2 className="w-3 h-3" />
</button>
<button
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
onClick={(e) => handleDeleteProfile(index, e)}
title="Delete Profile"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
)}
</li>
))}
<li className="border-t border-base-300 pt-1 mt-1">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
>
<Copy className="w-3.5 h-3.5" />
{transI18n("copyProfiles")}
</button>
</li>
<li>
<button
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
>
<Plus className="w-3.5 h-3.5" />
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
{/* Quick Profile Actions */}
<div className="flex items-center border-l border-base-content/10 pl-1 pr-0.5 gap-0.5 h-5">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="btn btn-ghost btn-xs btn-circle h-6 w-6 min-h-6 text-base-content/60 hover:text-primary hover:bg-primary/10"
title={transI18n("copyProfiles")}
>
<Copy className="w-3.5 h-3.5" />
</button>
<button
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
className="btn btn-ghost btn-xs btn-circle h-6 w-6 min-h-6 text-base-content/60 hover:text-primary hover:bg-primary/10"
title={transI18n("addNewProfile")}
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Desktop Connect PS Button */}
<div className="hidden sm:flex items-center gap-2 ml-auto tooltip tooltip-bottom" data-tip={isConnectPS ? transI18n("sync") : transI18n("connectPs")}>
<button
onClick={handleConnectOrSyncPS}
className={`btn btn-sm h-8.5 min-h-8.5 rounded-xl px-3.5 gap-1.5 font-bold shadow-xs whitespace-nowrap active-press ${
isConnectPS
? "btn-outline btn-accent"
: "btn-primary shadow-sm"
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${isConnectPS ? "text-accent" : ""}`} />
<span className="text-xs">{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</button>
</div>
</div>
{/* Mobile-Only Action Row (< 640px): [Avatars Button] & [Actions Dropdown] */}
<div className="grid grid-cols-2 sm:hidden gap-2 w-full pt-1">
<button
onClick={() => {
setIsOpenAvatars(true);
handleShow("avatars_modal");
}}
className="btn btn-warning btn-sm w-full font-bold shadow-xs"
>
{transI18n("avatars")}
</button>
<div className="dropdown dropdown-end w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full font-bold shadow-xs">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl w-56 mt-2 border border-base-300 space-y-1"
>
{navTabs.map((tab) => {
const Icon = tab.icon;
return (
<li key={tab.path}>
<button
onClick={() => actionMove(tab.path)}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold"
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
</li>
);
})}
<li className="border-t border-base-300 pt-1 mt-1">
<button
onClick={handleConnectOrSyncPS}
className="btn btn-primary btn-sm w-full font-bold mt-1"
>
<RefreshCw className="w-3.5 h-3.5" />
<span>{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</button>
</li>
</ul>
</div>
</div>
{/* Navigation Tabs Row (>= 640px) */}
<div className="hidden sm:block w-full bg-base-200/80 border border-base-content/10 rounded-xl p-1 shadow-xs overflow-x-auto scrollbar-none">
<div className="flex sm:grid sm:grid-cols-5 w-full min-w-max gap-1">
{navTabs.map((tab) => {
const isActive = currentActivePath === tab.path;
const Icon = tab.icon;
return (
<button
key={tab.path}
onClick={() => actionMove(tab.path)}
className={`flex items-center justify-center gap-2 py-2 px-3 rounded-lg text-xs font-bold transition-all duration-200 whitespace-nowrap active-press ${
isActive
? "bg-primary text-primary-content shadow-xs"
: "text-base-content/75 hover:text-base-content hover:bg-base-100"
}`}
>
<Icon className="w-4 h-4 shrink-0" />
<span>{tab.label}</span>
</button>
);
})}
</div>
</div>
{/* Modals */}
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[92vw] sm:max-w-[88vw] max-h-[90vh] bg-base-100 text-base-content border border-base-300 shadow-2xl rounded-2xl p-0 overflow-hidden">
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-300 bg-base-100">
<h3 className="font-bold text-xl tracking-tight text-primary">
{title}
</h3>
<button
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
onClick={onClose}
>
✕
</button>
</div>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
{content}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog>
))}
</div>
);
}
+199 -140
View File
@@ -1,140 +1,199 @@
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const {locale} = useLocaleStore()
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="grid grid-flow-row h-full auto-rows-max w-full">
<div className="h-full rounded-lg mx-2 py-2">
<div className="container">
<div className="flex flex-col h-full gap-2">
<div className="flex flex-col gap-2">
<div className="flex justify-center">
<input type="text"
placeholder={transI18n("placeholderCharacter")}
className="input input-bordered input-primary w-full"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
</div>
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
style={{
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
</div>
<div className="flex items-start h-full">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
{listAvatar.map((item, index) => (
<div key={index} onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
import { useShallow } from "zustand/react/shallow";
import { Search, X } from "lucide-react"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore(
useShallow((state) => ({
avatarSearch: state.avatarSearch,
mapAvatarElementActive: state.mapAvatarElementActive,
mapAvatarPathActive: state.mapAvatarPathActive,
setAvatarSearch: state.setAvatarSearch,
setAvatarSelected: state.setAvatarSelected,
setMapAvatarElementActive: state.setMapAvatarElementActive,
setMapAvatarPathActive: state.setMapAvatarPathActive,
setSkillIDSelected: state.setSkillIDSelected,
}))
)
const { mapAvatar, baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
mapAvatar: state.mapAvatar,
baseType: state.baseType,
damageType: state.damageType,
}))
)
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="flex flex-col h-full gap-2 p-2">
{/* Compact Search Bar */}
<label className="input input-sm input-bordered flex items-center gap-2 bg-base-200/90 border-base-300 rounded-xl w-full h-8.5 px-3 shadow-xs focus-within:border-primary/60 transition-colors">
<Search className="w-4 h-4 text-base-content/70 shrink-0" />
<input
type="text"
placeholder={transI18n("placeholderCharacter")}
className="grow text-xs bg-transparent border-none outline-none focus:outline-none placeholder:text-base-content/50"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
{avatarSearch && (
<button
type="button"
onClick={() => setAvatarSearch("")}
className="text-base-content/50 hover:text-base-content"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</label>
{/* Compact Filter Section */}
<div className="p-1 rounded-xl bg-base-200/80 border border-base-content/15 space-y-1 shadow-xs">
{/* Element Filters - 7 Elements on 1 Row (gap-0.5) */}
<div className="grid grid-cols-7 gap-0.5">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapAvatarElementActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className={`h-8.5 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
isActive
? "bg-primary/20 text-primary ring-1.5 ring-primary scale-105 z-10 shadow-xs"
: "hover:bg-base-100 opacity-90 hover:opacity-100"
}`}
title={value.DamageTypeName || key}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="w-6.5 h-6.5 object-contain drop-shadow-xs"
width={32}
height={32}
/>
</button>
);
})}
</div>
{/* Path Filters - 9 Paths on 1 Row (gap-0.5) */}
<div className="grid grid-cols-9 gap-0.5">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapAvatarPathActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className={`h-8 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
isActive
? "bg-primary/20 ring-1.5 ring-primary scale-105 z-10 shadow-xs"
: "hover:bg-base-100 opacity-90 hover:opacity-100"
}`}
title={value.BaseTypeName || key}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className={`w-5.5 h-5.5 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={28}
height={28}
/>
</button>
);
})}
</div>
</div>
{/* Character Count & Grid List */}
<div className="flex items-center justify-between text-xs font-semibold text-base-content/80 px-1 py-0.5">
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse"></span>
<span>{transI18n("character")}</span>
</span>
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded-md bg-primary/10 text-primary border border-primary/20">
{listAvatar.length}
</span>
</div>
<div className="flex-1 overflow-y-auto pr-0.5 scrollbar-thin">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-1.5 auto-rows-fr">
{listAvatar.map((item) => (
<div
key={item.ID}
className="h-full flex flex-col"
onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}
>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+116 -83
View File
@@ -1,83 +1,116 @@
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore();
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore()
return (
<li
className="z-10 flex flex-col items-center rounded-xl shadow-xl
bg-linear-to-br from-base-300 via-base-100 to-warning/70
transform transition-transform duration-300 ease-in-out
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<div className="relative w-full h-32 lg:h-26 xl:h-36">
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="rounded-md w-full h-full object-contain"
alt="ALT"
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.DamageType.toLowerCase()}
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, data)}
className="
w-full px-0.5
my-1
text-center font-bold text-shadow-white
leading-tight
wrap-break-word
text-sm sm:text-base 2xl:text-lg
"
/>
</li>
);
}
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
import useUserDataStore from '@/stores/userDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
baseType: state.baseType,
damageType: state.damageType,
}))
)
const { avatars } = useUserDataStore(
useShallow((state) => ({ avatars: state.avatars }))
)
const userAvatar = avatars[data.ID.toString()]
const level = userAvatar?.level
const rank = userAvatar?.data?.rank ?? 0
const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
const is4Star = data.Rarity === "CombatPowerAvatarRarityType4";
return (
<li
className="group relative flex flex-col items-center rounded-xl overflow-hidden bg-base-100 border border-base-content/10 shadow-sm hover:shadow-md cursor-pointer select-none h-full"
>
{/* Artwork container with vibrant rarity background */}
<div
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: is4Star
? "from-purple-400 via-purple-600/80 to-purple-800/60"
: "from-sky-400 via-blue-600/80 to-blue-800/60"
}`}
>
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="w-full h-full object-contain transition-transform duration-300 ease-out group-hover:scale-105"
alt={data.ID.toString()}
/>
{/* Level and Eidolons Badge Overlay */}
{level && (
<div className="absolute bottom-1 left-1 flex items-center gap-1 z-10">
<span className="bg-black/60 backdrop-blur-xs text-white text-[10px] font-bold font-mono px-1.5 py-0.2 rounded shadow-xs">
{level}
</span>
{rank > 0 && (
<span className="bg-amber-500/80 text-black text-[10px] font-black font-mono px-1 py-0.2 rounded shadow-xs">
E{rank}
</span>
)}
</div>
)}
{/* Damage Type Badge */}
{damageType?.[data.DamageType]?.Icon && (
<Image
width={28}
height={28}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
className="absolute top-1 left-1 w-6 h-6 object-contain drop-shadow-sm"
alt={data.DamageType.toLowerCase()}
/>
)}
{/* Base Type / Path Badge */}
{baseType?.[data.BaseType]?.Icon && (
<Image
width={28}
height={28}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
className="absolute top-1 right-1 w-6 h-6 object-contain rounded-full drop-shadow-sm"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 6px 3px rgba(255, 255, 255, 0.4)"
}}
/>
)}
</div>
{/* Name Label - Clear Full Name Display */}
<div className="w-full py-1.5 px-1 text-center flex items-center justify-center bg-base-100 min-h-11">
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, data)}
className="font-bold text-xs sm:text-[11.5px] lg:text-xs leading-tight px-0.5 text-base-content group-hover:text-primary transition-colors text-center w-full"
/>
</div>
</li>
);
}
+156 -124
View File
@@ -1,125 +1,157 @@
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onCharacterToggle(character)}
>
{/* Character Portrait */}
<div className="relative mb-4">
<div className="w-full h-48 rounded-lg overflow-hidden relative">
<Image
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
className="w-full h-full object-contain"
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
{/* Character Name and Level */}
<div className="w-full rounded-lg flex items-center justify-center mb-2">
<div className="text-center">
<ParseText className="text-lg font-bold"
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
locale={locale}
/>
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
</div>
</div>
{character.relics.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{character.relics.map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone.item_id !== 0 && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
</div>
</div>
</div>
)}
</div>
);
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
import { useShallow } from 'zustand/react/shallow';
export default function CharacterInfoCard({
character,
selectedCharacters,
onCharacterToggle
}: {
character: CharacterInfoCardType,
selectedCharacters: CharacterInfoCardType[],
onCharacterToggle: (characterId: CharacterInfoCardType) => void
}) {
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
mapAvatar: state.mapAvatar,
mapLightCone: state.mapLightCone,
baseType: state.baseType,
damageType: state.damageType,
}))
);
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
);
const avatar = mapAvatar?.[character.avatar_id.toString()];
return (
<div
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
isSelected
? 'bg-primary/10 border-primary shadow-lg shadow-primary/20 ring-2 ring-primary/40'
: 'bg-base-200/60 border-white/5 hover:border-white/20 opacity-80 hover:opacity-100 hover:bg-base-200'
}`}
onClick={() => onCharacterToggle(character)}
>
{/* Character Portrait */}
<div className="relative mb-3 aspect-[4/5] rounded-xl overflow-hidden bg-base-300/40 flex items-center justify-center">
{avatar && (
<>
<Image
src={`${process.env.CDN_URL}/${avatar.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, avatar.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
/>
{damageType?.[avatar.DamageType]?.Icon && (
<div className="absolute top-1.5 left-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
<Image
width={24}
height={24}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
className="w-5 h-5 object-contain"
alt={avatar.DamageType.toLowerCase()}
/>
</div>
)}
{baseType?.[avatar.BaseType]?.Icon && (
<div className="absolute top-1.5 right-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
<Image
width={24}
height={24}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
className="w-5 h-5 object-contain"
alt={avatar.BaseType.toLowerCase()}
/>
</div>
)}
</>
)}
</div>
{/* Character Name and Level Badge */}
<div className="text-center mb-3">
<ParseText
className="text-base font-bold line-clamp-1"
text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
locale={locale}
/>
<div className="flex items-center justify-center gap-2 mt-1">
<span className="text-xs font-mono font-semibold px-2 py-0.5 rounded-full bg-base-300/80 text-base-content/80">
Lv.{character.level}
</span>
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30">
E{character.rank}
</span>
</div>
</div>
{/* Relics Mini-grid */}
{character.relics.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-1.5 mb-3 p-1.5 rounded-xl bg-base-100/50 border border-white/5">
{character.relics.map((relic, index) => (
<div key={index} className="relative group/relic">
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
<Image
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
className="w-6 h-6 object-contain"
/>
</div>
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 bg-black/80 text-[9px] font-mono font-bold text-amber-400 px-1 rounded-full border border-white/10">
+{relic.level}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone && character.lightcone.item_id !== 0 && (
<div className="p-2 rounded-xl bg-base-100/50 border border-white/5 text-center">
<div className="h-28 flex items-center justify-center mb-1">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt="Lightcone"
width={160}
height={200}
className="h-full w-auto object-contain rounded-lg"
/>
</div>
<div className="text-xs font-bold truncate">
<ParseText
text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="flex items-center justify-center gap-2 mt-1">
<span className="text-[11px] font-mono text-base-content/70">Lv.{character.lightcone.level}</span>
<span className="text-[11px] font-mono font-bold text-primary">S{character.lightcone.rank}</span>
</div>
</div>
)}
</div>
);
};
+55 -53
View File
@@ -1,53 +1,55 @@
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getLocaleName(locale, data.Name)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
hover:scale-105 cursor-pointer min-h-55"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
"from-blue-400 via-blue-600/70 to-blue-800/50"
}`}
>
<div className="relative w-full h-full">
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
</div>
</div>
<ParseText
locale={locale}
text={text}
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
/>
</li>
);
}
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { useShallow } from 'zustand/react/shallow';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
const text = getLocaleName(locale, data.Name);
const is5Star = data.Rarity === "CombatPowerLightconeRarity5";
const is4Star = data.Rarity === "CombatPowerLightconeRarity4";
return (
<li className="group relative flex flex-col items-center rounded-xl overflow-hidden bg-base-100 border border-base-content/10 shadow-sm hover:shadow-md cursor-pointer select-none h-full">
{/* Artwork container with vibrant rarity background */}
<div
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: is4Star
? "from-purple-400 via-purple-600/80 to-purple-800/60"
: "from-sky-400 via-blue-600/80 to-blue-800/60"
}`}
>
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
alt={data?.Name?.en || "Lightcone"}
/>
</div>
{/* Name Label - Uniform Fixed-Height Footer */}
<div className="w-full h-11 px-1 py-0.5 text-center flex items-center justify-center bg-base-100">
<ParseText
locale={locale}
text={text}
className="font-bold text-xs leading-tight line-clamp-2 text-base-content group-hover:text-primary transition-colors text-center"
/>
</div>
</li>
);
}
+89 -78
View File
@@ -1,79 +1,90 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onProfileToggle(profile)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
</div>
</div>
</div>
)}
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
</div>
);
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
import { useShallow } from 'zustand/react/shallow';
export default function ProfileCard({
profile,
selectedProfile,
onProfileToggle
}: {
profile: AvatarProfileCardType,
selectedProfile: AvatarProfileCardType[],
onProfileToggle: (profileId: AvatarProfileCardType) => void
}) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore(
useShallow((state) => ({ mapLightCone: state.mapLightCone }))
);
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
);
return (
<div
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
isSelected
? 'bg-primary/10 border-primary shadow-md ring-2 ring-primary/40'
: 'bg-base-200/80 border-base-300 hover:border-primary/40 hover:bg-base-200'
}`}
onClick={() => onProfileToggle(profile)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="p-2 rounded-xl bg-base-100 border border-base-300 text-center mb-3">
<div className="h-32 flex items-center justify-center mb-2">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt="Lightcone"
width={240}
height={300}
className="h-full w-auto object-contain rounded-lg transition-transform duration-200 group-hover:scale-105"
/>
</div>
<div className="text-sm font-bold truncate">
<ParseText
text={getLocaleName(locale, mapLightCone?.[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="flex items-center justify-center gap-2 mt-1">
<span className="text-xs font-mono font-medium text-base-content/70">Lv.{profile.lightcone.level}</span>
<span className="text-xs font-mono font-bold text-primary">S{profile.lightcone.rank}</span>
</div>
</div>
)}
{/* Relics Mini-grid */}
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-1.5 p-1.5 rounded-xl bg-base-100 border border-base-300">
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative group/relic">
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
width={32}
height={32}
className="w-6 h-6 object-contain"
/>
</div>
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 bg-black/80 text-[9px] font-mono font-bold text-amber-400 px-1 rounded-full border border-white/10">
+{relic.level}
</div>
</div>
))}
</div>
)}
</div>
);
};
+134 -195
View File
@@ -1,196 +1,135 @@
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
default: return 'border-gray-500 shadow-gray-500/50';
}
};
const getRarityName = (slot: string) => {
switch (slot) {
case '1': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HEAD.png"
alt="Head"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Head</h2>
</div>
);
case '2': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HAND.png"
alt="Hand"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/BODY.png"
alt="Body"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/FOOT.png"
alt="Foot"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/NECK.png"
alt="Neck"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/OBJECT.png"
alt="Object"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Link rope</h2>
</div>
);
default: return '';
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { avatars } = useUserDataStore()
const { selectedRelicSlot } = useRelicMakerStore()
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
return avatar.profileList[avatar.profileSelect].relics[slot];
}
return null;
}
return null;
}, [avatars, avatarId, slot]);
return (
<div>
{relicDetail ? (
<div
className="flex flex-col items-center cursor-pointer ">
<div
className={`
relative w-24 h-24 rounded-full
${getRarityColor(relicDetail.relic_id.toString()[0])}
shadow-xl
flex items-center justify-center
cursor-pointer transition-transform
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
`}
>
<span>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
<div className="mt-3 text-center">
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
</div>
</div>
) : (
<div
className="flex flex-col items-center cursor-pointer">
<div
className={`
relative w-24 h-24 rounded-full border-4
${getRarityColor("None")}
bg-base-300 shadow-xl
flex items-center justify-center
cursor-pointer hover:scale-105 transition-transform
ring-4 ring-primary
`}
>
<span className="text-3xl">
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{0}</span>
</div>
</div>
<div className="mt-3 text-center">
<div>{getRarityName(slot)}</div>
</div>
</div>
)}
</div>
)
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import { useShallow } from "zustand/react/shallow";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case '6':
return 'border-amber-400/90 shadow-[0_0_16px_rgba(251,191,36,0.4)] bg-linear-to-br from-amber-600 via-amber-400 to-amber-500';
case '5':
return 'border-purple-400/90 shadow-[0_0_16px_rgba(192,132,252,0.4)] bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
case '4':
return 'border-sky-400/90 shadow-[0_0_16px_rgba(56,189,248,0.4)] bg-linear-to-br from-blue-700 via-blue-400 to-sky-500';
case '3':
return 'border-emerald-400/90 shadow-[0_0_16px_rgba(52,211,153,0.4)] bg-linear-to-br from-emerald-700 via-emerald-400 to-emerald-500';
default:
return 'border-base-content/20 bg-base-300';
}
};
const getSlotIconAndName = (slot: string) => {
const slotMap: Record<string, { icon: string; name: string }> = {
'1': { icon: '/relics/HEAD.png', name: 'Head' },
'2': { icon: '/relics/HAND.png', name: 'Hands' },
'3': { icon: '/relics/BODY.png', name: 'Body' },
'4': { icon: '/relics/FOOT.png', name: 'Feet' },
'5': { icon: '/relics/NECK.png', name: 'Planar Sphere' },
'6': { icon: '/relics/OBJECT.png', name: 'Link Rope' },
};
const current = slotMap[slot];
if (!current) return null;
return (
<div className="flex items-center gap-1.5 justify-center mt-2.5">
<Image
unoptimized
crossOrigin="anonymous"
src={current.icon}
alt={current.name}
width={18}
height={18}
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
/>
<span className="text-xs font-bold text-base-content">{current.name}</span>
</div>
);
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { avatars } = useUserDataStore(
useShallow((state) => ({ avatars: state.avatars }))
)
const { selectedRelicSlot } = useRelicMakerStore(
useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
)
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
return profile?.relics?.[slot] || null;
}
return null;
}, [avatars, avatarId, slot]);
const isSelected = selectedRelicSlot === slot;
return (
<div className="flex flex-col items-center select-none group cursor-pointer">
{relicDetail ? (
<div className="flex flex-col items-center">
<div
className={`
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-3
${getRarityColor(relicDetail.relic_id.toString()[0])}
flex items-center justify-center
transition-all duration-200
${isSelected ? 'ring-2 ring-primary/70 shadow-md ring-offset-2 ring-offset-base-100' : 'hover:border-primary/60'}
`}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 sm:w-16 sm:h-16 object-contain drop-shadow-md"
/>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
<span className="text-xs font-mono font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
{getSlotIconAndName(slot)}
</div>
) : (
<div className="flex flex-col items-center">
<div
className={`
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-2 border-dashed border-base-content/20
bg-base-200/60 flex items-center justify-center
transition-all duration-200 group-hover:border-primary/60
${isSelected ? 'ring-2 ring-primary/70 border-solid bg-primary/10 ring-offset-2 ring-offset-base-100' : ''}
`}
>
<div className="flex flex-col items-center justify-center gap-1 text-base-content/40 group-hover:text-primary transition-colors">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
<span className="text-xs font-mono font-bold text-base-content/40">+0</span>
</div>
</div>
{getSlotIconAndName(slot)}
</div>
)}
</div>
)
}
+83 -68
View File
@@ -1,69 +1,84 @@
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore()
return (
<div
onClick={onClick}
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<Image
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain"
alt="Avatar"
/>
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</div>
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
{showRemoveHover && (
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
)}
</div>
);
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
import { X } from 'lucide-react';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
baseType: state.baseType,
damageType: state.damageType,
}))
)
const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
return (
<div
onClick={onClick}
className={`group relative w-16 h-16 sm:w-20 sm:h-20 rounded-xl cursor-pointer shadow-sm hover:shadow-md shrink-0 border border-base-content/10 overflow-hidden select-none ${
isSelected ? 'ring-2 ring-primary opacity-60 scale-95' : ''
}`}
>
<div
className={`relative w-full h-full rounded-xl overflow-hidden flex items-center justify-center bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: "from-purple-400 via-purple-600/80 to-purple-800/60"
}`}
>
<Image
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain transition-transform duration-200 group-hover:scale-105"
alt="Avatar"
/>
{damageType?.[data.DamageType]?.Icon && (
<div className="absolute top-0.5 left-0.5 w-4 h-4 sm:w-5 sm:h-5 bg-black/60 backdrop-blur-xs rounded-full flex items-center justify-center p-0.5 border border-white/10">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</div>
)}
{baseType?.[data.BaseType]?.Icon && (
<div className="absolute top-0.5 right-0.5 w-4 h-4 sm:w-5 sm:h-5 bg-black/60 backdrop-blur-xs rounded-full flex items-center justify-center p-0.5 border border-white/10">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
)}
{showRemoveHover && (
<div className="absolute inset-0 bg-error/90 backdrop-blur-xs rounded-xl flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<X className="h-6 w-6 text-white" />
</div>
)}
</div>
</div>
);
};
+85 -90
View File
@@ -1,91 +1,86 @@
import useLocaleStore from '@/stores/localeStore';
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore()
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
case 'fix':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
case 'improvement':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
default:
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'badge-success';
case 'fix':
return 'badge-error';
case 'improvement':
return 'badge-warning';
default:
return 'badge-info';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="md:p-4">
{/* Alert */}
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
<AlertCircle className="w-6 h-6" />
</div> */}
{/* Timeline */}
<div className="flex flex-col gap-4">
{changelog.map((change, index) => (
<div key={index} className="bg-base-100 shadow-sm">
<div className="flex flex-col gap-2">
{/* Version Header */}
<div className="flex flex-row items-center gap-2 md:gap-3">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version}
</h2>
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-3">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3">
<div className="mt-1">
<Check className="w-5 h-5 text-success" />
</div>
<span className="text-base-content">{item}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
);
import useLocaleStore from '@/stores/localeStore';
import { useShallow } from 'zustand/react/shallow';
import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore(
useShallow((state) => ({ changelog: state.changelog }))
)
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3.5 h-3.5" />;
case 'fix':
return <Bug className="w-3.5 h-3.5" />;
case 'improvement':
return <Zap className="w-3.5 h-3.5" />;
default:
return <Package className="w-3.5 h-3.5" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30';
case 'fix':
return 'bg-rose-500/15 text-rose-400 border-rose-500/30';
case 'improvement':
return 'bg-amber-500/15 text-amber-400 border-amber-500/30';
default:
return 'bg-primary/15 text-primary border-primary/30';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="space-y-4">
<div className="space-y-3">
{changelog.map((change, index) => (
<div key={index} className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-3">
{/* Version Header */}
<div className="flex flex-wrap items-center justify-between gap-2 pb-2.5 border-b border-base-content/10">
<div className="flex items-center gap-2">
<span className={`px-2.5 py-1 rounded-full text-xs font-mono font-bold flex items-center gap-1.5 border ${getBadgeClass(change.type)}`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</span>
<h3 className="font-extrabold text-sm sm:text-base tracking-tight">
Version {change.version}
</h3>
</div>
<div className="flex items-center gap-1.5 text-xs text-base-content/50 font-mono">
<Calendar className="w-3.5 h-3.5" />
<span>{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-2 pt-1 text-xs sm:text-sm text-base-content/85">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-2.5">
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
<span className="leading-relaxed">{item}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}
+55 -55
View File
@@ -1,56 +1,56 @@
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
}
+202 -168
View File
@@ -1,169 +1,203 @@
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
)
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
import { useShallow } from "zustand/react/shallow"
import { Server, RefreshCw, AlertCircle } from "lucide-react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore(
useShallow((state) => ({
connectionType: state.connectionType,
privateType: state.privateType,
serverUrl: state.serverUrl,
username: state.username,
password: state.password,
setConnectionType: state.setConnectionType,
setPrivateType: state.setPrivateType,
setServerUrl: state.setServerUrl,
setUsername: state.setUsername,
setPassword: state.setPassword,
}))
)
const { isConnectPS, setIsConnectPS } = useGlobalStore(
useShallow((state) => ({
isConnectPS: state.isConnectPS,
setIsConnectPS: state.setIsConnectPS,
}))
)
return (
<div className="space-y-4">
<div className="p-4 sm:p-6 rounded-2xl bg-base-200/60 border border-white/10 space-y-4">
{/* Connection Type Selection */}
<div className="space-y-1.5">
<label className="block text-xs font-bold uppercase tracking-wider text-base-content/70">
{transI18n("connectionType")}
</label>
<select
className="select select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-sm font-medium"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("serverUrl")}
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("privateType")}
</label>
<select
className="select select-sm sm:select-md select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("username")}
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("password")}
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{/* Status Message Notification */}
{message.text && (
<div className={`p-3 rounded-xl border text-xs flex items-center gap-2 ${
message.type === 'success'
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
: message.type === 'error'
? 'bg-rose-500/10 border-rose-500/30 text-rose-400'
: 'bg-info/10 border-info/30 text-info'
}`}>
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{message.text}</span>
</div>
)}
{/* Footer Controls */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-3 border-t border-base-content/10">
<div className="flex items-center gap-2">
<span className="text-xs text-base-content/60">{transI18n("status")}:</span>
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-mono font-bold border ${
isConnectPS
? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
: "bg-rose-500/15 border-rose-500/30 text-rose-400"
}`}>
<span className={`w-2 h-2 rounded-full ${isConnectPS ? 'bg-emerald-400 animate-pulse' : 'bg-rose-400'}`}></span>
<span>{isConnectPS ? transI18n("connected") : transI18n("unconnected")}</span>
</div>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-sm btn-primary rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
>
<Server className="w-3.5 h-3.5" />
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-sm btn-accent rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
>
<RefreshCw className="w-3.5 h-3.5" />
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
</div>
)
}
+142 -85
View File
@@ -1,85 +1,142 @@
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore()
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
return (
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("eidolons")}
</h2>
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
{charRank && avatars[avatarSelected?.ID || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(charRank || {}).map(([key, rank]) => (
<div key={key}
className="flex flex-col items-center"
>
<div
className="cursor-pointer"
onClick={() => {
let newRank = Number(rank.Rank)
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
newRank = Number(rank.Rank) - 1
}
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
}}
>
<Image
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
<ParseText
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="text-center text-base font-normal leading-tight"
/>
</div>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
<ExtraEffectList extras={rank?.Extra} locale={locale} />
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
import { useShallow } from "zustand/react/shallow";
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
)
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore(
useShallow((state) => ({
setAvatars: state.setAvatars,
avatars: state.avatars,
}))
)
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
const currentRank = avatars[avatarSelected?.ID.toString() || ""]?.data?.rank || 0;
const handleToggleRank = (rankNum: number) => {
if (!avatarSelected) return;
let newRank = rankNum;
if (currentRank === rankNum) {
newRank = rankNum - 1;
}
setAvatars({
...avatars,
[avatarSelected.ID]: {
...avatars[avatarSelected.ID],
data: {
...avatars[avatarSelected.ID]?.data,
rank: newRank,
},
},
});
};
return (
<div className="w-full">
<div className="bg-base-100 rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-6">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-base-300">
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
{transI18n("eidolons")}
</span>
</h2>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-base-200 border border-base-300 text-xs font-mono font-bold">
<span className="text-base-content/60">Active:</span>
<span className="text-primary text-sm font-extrabold">E{currentRank}</span>
</div>
</div>
{/* 6 Eidolons Organic Grid */}
{charRank && avatars[avatarSelected?.ID || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{Object.entries(charRank || {}).map(([key, rank]) => {
const rankNum = Number(rank.Rank);
const isUnlocked = rankNum <= currentRank;
return (
<div
key={key}
className="flex flex-col items-center text-center space-y-3 p-2"
>
{/* 1. Organic Floating Eidolon Shard (No artificial rectangle wrapper) */}
<div
onClick={() => handleToggleRank(rankNum)}
className="cursor-pointer select-none transition-transform duration-200 hover:scale-105 active:scale-95"
title={`Click to ${isUnlocked ? 'deactivate' : 'activate'} E${rank.Rank}`}
>
<Image
className={`w-44 sm:w-52 h-auto object-contain drop-shadow-md transition-all duration-300 ${
isUnlocked
? "drop-shadow-[0_0_12px_rgba(var(--color-primary),0.35)]"
: "grayscale opacity-40 hover:opacity-70"
}`}
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
</div>
{/* 2. Eidolon Name & Rank */}
<div
onClick={() => handleToggleRank(rankNum)}
className="flex items-center justify-center gap-1.5 cursor-pointer group select-none"
>
<span className={`text-base font-extrabold font-mono ${
isUnlocked ? "text-primary" : "text-base-content/50"
}`}>
E{rank.Rank}.
</span>
<ParseText
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="font-bold text-base text-base-content group-hover:text-primary transition-colors"
/>
</div>
{/* 3. Description & Extra Effects (Direct readable text, fully selectable & copyable) */}
<div className="w-full text-left text-xs sm:text-sm text-base-content/85 leading-relaxed space-y-2 select-text pt-1 border-t border-base-300/60">
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param),
}}
/>
<ExtraEffectList extras={rank?.Extra} locale={locale} />
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
+98 -98
View File
@@ -1,99 +1,99 @@
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+18 -9
View File
@@ -1,9 +1,18 @@
export default function Footer() {
return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside>
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
</aside>
</footer>
)
}
export default function Footer() {
return (
<footer className="w-full border-t border-base-content/10 bg-base-100 py-6 px-4 mt-auto">
<div className="max-w-[1920px] mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-base-content/60 font-medium">
<div className="flex items-center gap-2">
<span className="font-semibold text-base-content/80">Firefly Shelter</span>
<span>•</span>
<span>S Game Toolkit</span>
</div>
<div className="flex items-center gap-2">
<span>© {new Date().getFullYear()}</span>
<span>•</span>
<span className="px-2 py-0.5 rounded-full bg-base-200 text-base-content/70 border border-white/5">Next.js & DaisyUI</span>
</div>
</div>
</footer>
)
}
File diff suppressed because it is too large Load Diff
+324 -267
View File
@@ -1,74 +1,107 @@
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
const { avatarSelected } = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
);
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
}))
);
const { mapAvatar, baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
mapAvatar: state.mapAvatar,
baseType: state.baseType,
damageType: state.damageType,
}))
);
const { locale } = useLocaleStore(
useShallow((state) => ({
locale: state.locale,
}))
);
const {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore(
useShallow((state) => ({
selectedProfiles: state.selectedProfiles,
avatarCopySelected: state.avatarCopySelected,
setSelectedProfiles: state.setSelectedProfiles,
setAvatarCopySelected: state.setAvatarCopySelected,
listElement: state.listElement,
listPath: state.listPath,
listRank: state.listRank,
setListElement: state.setListElement,
setListPath: state.setListPath,
setListRank: state.setListRank,
}))
);
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
const selectAll = () => {
if (avatarCopySelected) {
const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
@@ -79,32 +112,32 @@ export default function CopyImport() {
return null;
}
return {
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
if (!avatarSelected) {
setMessage({
type: "error",
@@ -129,181 +162,205 @@ export default function CopyImport() {
if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) {
return null;
}
return {
...profile,
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
} as AvatarProfileStore
}).filter((profile) => profile !== null);
const cloned = structuredClone(profile);
cloned.profile_name = profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`;
return cloned as AvatarProfileStore;
}).filter((profile): profile is AvatarProfileStore => profile !== null);
const newAvatar = {
...targetAvatar,
profileList: targetAvatar.profileList.concat(newListProfile),
profileList: [...targetAvatar.profileList, ...newListProfile],
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
}
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-lg px-2 min-h-[60vh]">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-2">
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>{transI18n("filter")}</div>
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
{/* Path */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListPath({ ...listPath, [key]: !listPath[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-8 w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Element */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListElement({ ...listElement, [key]: !listElement[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Rank */}
<div>
<div className="flex flex-wrap gap-2 justify-end items-center">
{Object.entries(listRank).map(([key], index) => (
<div
key={index}
onClick={() => {
setListRank({ ...listRank, [key]: !listRank[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
}}>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
{listAvatar.length > 0 && (
<div>
<div>{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
</div>
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedProfiles")}: {selectedProfiles.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
onClick={handleCopy}
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
{transI18n("copy")}
</button>
)}
</div>
{message.type && message.text && (
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
)}
</div>
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-2xl p-2 sm:p-4 min-h-[50vh] space-y-4">
<div className="max-w-7xl mx-auto space-y-4">
{/* Header & Filters Card */}
<div className="bg-base-100 rounded-2xl p-4 border border-base-300 shadow-sm space-y-4">
{/* Filters Section */}
<div className="space-y-3">
<div className="text-xs font-bold uppercase tracking-wider text-base-content/60">{transI18n("filter")}</div>
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
{/* Path Filters */}
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!listPath[key];
return (
<button
key={key}
type="button"
onClick={() => setListPath({ ...listPath, [key]: !listPath[key] })}
className={`h-10 w-10 sm:h-12 sm:w-12 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center cursor-pointer ${
isActive
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
}`}
title={key}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className={`w-6 h-6 sm:w-7 sm:h-7 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={32}
height={32}
/>
</button>
);
})}
</div>
<div className="h-8 w-px bg-base-300 hidden md:block" />
{/* Element Filters */}
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!listElement[key];
return (
<button
key={key}
type="button"
onClick={() => setListElement({ ...listElement, [key]: !listElement[key] })}
className={`h-10 w-10 sm:h-12 sm:w-12 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center cursor-pointer ${
isActive
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
}`}
title={key}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="w-6 h-6 sm:w-7 sm:h-7 object-contain drop-shadow-xs"
width={32}
height={32}
/>
</button>
);
})}
</div>
<div className="h-8 w-px bg-base-300 hidden md:block" />
{/* Rank Filters */}
<div className="flex items-center gap-1.5 sm:gap-2">
{Object.entries(listRank).map(([key], index) => {
const isActive = !!listRank[key];
return (
<button
key={index}
type="button"
onClick={() => setListRank({ ...listRank, [key]: !listRank[key] })}
className={`h-10 px-3.5 sm:h-12 sm:px-4.5 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center text-xs sm:text-sm font-mono font-bold cursor-pointer ${
isActive
? "bg-primary text-primary-content border-primary shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 text-base-content/70 border-base-300"
}`}
>
{key}★
</button>
);
})}
</div>
</div>
</div>
{/* Character Select Dropdown */}
{listAvatar.length > 0 && (
<div className="space-y-1.5 pt-2 border-t border-base-300/60">
<div className="text-xs font-bold text-base-content/70">{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
{/* Selection Action Bar */}
<div className="bg-base-200 border border-base-300 rounded-2xl p-3.5 flex flex-wrap items-center justify-between gap-3 shadow-xs">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedProfiles")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedProfiles.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
onClick={handleCopy}
className="btn btn-xs sm:btn-sm btn-primary rounded-xl font-bold shadow-xs"
>
{transI18n("copy")}
</button>
)}
</div>
</div>
{message.type && message.text && (
<div className={`p-3 rounded-xl border text-xs sm:text-sm font-semibold flex items-center gap-2 ${
message.type === "error"
? "bg-error/10 text-error border-error/30"
: "bg-success/10 text-success border-success/30"
}`}>
<span>{message.type === "error" ? "⚠️" : "🎉"}</span>
<span>{message.text}</span>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
}
+173 -136
View File
@@ -1,10 +1,10 @@
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
import useModelStore from "@/stores/modelStore";
import { toast } from "react-toastify";
@@ -12,6 +12,7 @@ import { useTranslations } from "next-intl";
import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
import { z } from "zod";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
@@ -31,24 +32,46 @@ const toCharacterInfoCard = (character: AvatarEnkaDetailInput): CharacterInfoCar
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
})),
});
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const { mapAvatar } = useDetailDataStore()
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = useEnkaStore(
useShallow((state) => ({
uidInput: state.uidInput,
setUidInput: state.setUidInput,
enkaData: state.enkaData,
selectedCharacters: state.selectedCharacters,
setSelectedCharacters: state.setSelectedCharacters,
setEnkaData: state.setEnkaData,
}))
);
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
);
const { setIsOpenImport } = useModelStore(
useShallow((state) => ({
setIsOpenImport: state.setIsOpenImport,
}))
)
const { mapAvatar } = useDetailDataStore(
useShallow((state) => ({
mapAvatar: state.mapAvatar,
}))
)
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
const handlerFetchData = async () => {
if (!uidInput) {
setError(transI18n("pleaseEnterUid"))
@@ -77,36 +100,36 @@ export default function EnkaImport() {
setIsLoading(false)
}
}
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
if (enkaData) {
setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const parsed = enkaResponseSchema.safeParse(enkaData)
if (!parsed.success) {
setError(transI18n("failedToFetchEnkaData"));
@@ -115,90 +138,104 @@ export default function EnkaImport() {
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatarId.toString()] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level ?? 0
newAvatar.promotion = character.promotion ?? 0
newAvatar.data = {
rank: character.rank ?? 0,
skills: character.skillTreeList.reduce((acc, skill) => {
acc[skill.pointId] = skill.level;
return acc;
}, {} as Record<string, number>),
}
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
<div className="flex gap-4 items-center mb-6">
<input
type="text"
value={uidInput}
onChange={(e) => setUidInput(e.target.value)}
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
placeholder="Enter UID"
/>
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="text-white space-y-2">
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
const currentAvatar = listAvatars[character.avatarId.toString()]
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
const newProfileList = [...(currentAvatar.profileList || [])]
const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length)
if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level ?? 0,
promotion: character.promotion ?? 0,
data: {
rank: character.rank ?? 0,
skills: character.skillTreeList.reduce((acc, skill) => {
acc[skill.pointId] = skill.level;
return acc;
}, {} as Record<string, number>),
},
profileList: newProfileList,
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="space-y-4">
<h1 className="text-base-content text-xl sm:text-2xl font-bold">HSR UID</h1>
<div className="flex flex-wrap gap-3 items-center">
<input
type="text"
value={uidInput}
onChange={(e) => setUidInput(e.target.value)}
className="input input-bordered w-full max-w-xs rounded-xl bg-base-200 border-base-300 text-sm focus:border-primary"
placeholder="Enter UID"
/>
<button onClick={handlerFetchData} className="btn btn-sm btn-success rounded-xl font-bold">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="bg-base-200 p-4 rounded-xl border border-base-300 text-base-content space-y-1">
<div className="text-sm font-semibold">Name: <span className="font-bold text-primary">{enkaData.detailInfo.nickname}</span></div>
<div className="text-sm font-semibold">UID: <span className="font-mono">{enkaData.detailInfo.uid}</span></div>
<div className="text-sm font-semibold">Level: <span className="font-bold">{enkaData.detailInfo.level}</span></div>
</div>
)}
{/* Selection Info */}
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedCharacters")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
</div>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validCharacters.map((character) => (
<CharacterInfoCard
key={character.avatarId}
@@ -206,9 +243,9 @@ export default function EnkaImport() {
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
))}
</div>
</div>
</div>
);
))}
</div>
</div>
</div>
);
}
+262 -228
View File
@@ -1,228 +1,262 @@
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { z } from "zod";
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["avatars"][string]): CharacterInfoCardType => {
const lightcone = data.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = data.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
key: character.avatar_id,
avatar_id: character.avatar_id,
rank: character.data.rank ?? 0,
level: character.level,
lightcone: {
level: lightcone?.level ?? 0,
rank: lightcone?.rank ?? 0,
item_id: lightcone?.item_id ?? 0,
},
relics: relics.map((relic) => ({
level: relic.level,
relic_id: relic.relic_id,
relic_set_id: relic.relic_set_id,
})),
}
}
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore();
const { mapAvatar } = useDetailDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
const transI18n = useTranslations("DataPage")
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (parsed.success) {
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
}
};
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
const parsed = freeSrJsonSchema.safeParse(data)
if (!parsed.success) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
return
}
setFreeSRData(parsed.data)
setError("")
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
} catch {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
} finally {
setIsLoading(false)
}
};
reader.onerror = () => {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
}
reader.readAsText(file);
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (!parsed.success) {
setError(transI18n("fileMustBeAValidJsonFile"));
return;
}
const listAvatars = { ...avatars }
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
newAvatar.promotion = character.promotion
newAvatar.data = {
rank: character.data.rank ?? 0,
skills: character.data.skills
}
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
<div className="flex gap-4 items-center mb-6">
<fieldset className="fieldset">
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
return (
<CharacterInfoCard
key={character.avatar_id}
character={toCharacterInfoCard(validFreeSRData, character)}
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
)
})}
</div>
</div>
</div>
)
}
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
import { z } from "zod";
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["avatars"][string]): CharacterInfoCardType => {
const lightcone = data.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = data.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
key: character.avatar_id,
avatar_id: character.avatar_id,
rank: character.data.rank ?? 0,
level: character.level,
lightcone: {
level: lightcone?.level ?? 0,
rank: lightcone?.rank ?? 0,
item_id: lightcone?.item_id ?? 0,
},
relics: relics.map((relic) => ({
level: relic.level,
relic_id: relic.relic_id,
relic_set_id: relic.relic_set_id,
})),
}
}
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
);
const { mapAvatar } = useDetailDataStore(
useShallow((state) => ({
mapAvatar: state.mapAvatar,
}))
);
const { setIsOpenImport } = useModelStore(
useShallow((state) => ({
setIsOpenImport: state.setIsOpenImport,
}))
)
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore(
useShallow((state) => ({
freeSRData: state.freeSRData,
setFreeSRData: state.setFreeSRData,
selectedCharacters: state.selectedCharacters,
setSelectedCharacters: state.setSelectedCharacters,
}))
)
const transI18n = useTranslations("DataPage")
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (parsed.success) {
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
}
};
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
const parsed = freeSrJsonSchema.safeParse(data)
if (!parsed.success) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
return
}
setFreeSRData(parsed.data)
setError("")
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
} catch {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
} finally {
setIsLoading(false)
}
};
reader.onerror = () => {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
}
reader.readAsText(file);
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (!parsed.success) {
setError(transI18n("fileMustBeAValidJsonFile"));
return;
}
const listAvatars = { ...avatars }
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const currentAvatar = listAvatars[character.avatar_id]
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
const newProfileList = [...(currentAvatar.profileList || [])]
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id)
if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level,
promotion: character.promotion,
data: {
rank: character.data.rank ?? 0,
skills: character.data.skills
},
profileList: newProfileList,
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="space-y-4">
<h1 className="text-base-content text-xl sm:text-2xl font-bold">{transI18n("freeSRImport")}</h1>
<div className="flex flex-wrap gap-4 items-center">
<fieldset className="fieldset">
<legend className="fieldset-legend text-xs font-semibold">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-sm file-input-bordered rounded-xl" onChange={handlerReadFile} />
<label className="label text-[11px] text-base-content/60">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedCharacters")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
</div>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
return (
<CharacterInfoCard
key={character.avatar_id}
character={toCharacterInfoCard(validFreeSRData, character)}
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
)
})}
</div>
</div>
</div>
)
}
+211 -144
View File
@@ -1,145 +1,212 @@
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
export default function LightconeBar() {
const { locale } = useLocaleStore()
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { setIsOpenLightcone } = useModelStore()
const { mapLightCone, baseType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item =>
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || mapLightconePathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("lightConeSetting")}
</h3>
</div>
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2">
<div>Search</div>
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
/>
</div>
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>Filter</div>
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.keys(mapLightconeRankActive).map((key, index) => (
<div
key={index}
onClick={() => {
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
}}
>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
{key}*
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
{listLightcone.map((item, index) => (
<div key={index} onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected?.ID?.toString()]
avatar.profileList[avatar.profileSelect].lightcone = {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
setAvatar({ ...avatar })
setIsOpenLightcone(false)
}
}}>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
import { useShallow } from "zustand/react/shallow";
import { Search, X } from "lucide-react";
export default function LightconeBar() {
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
mapLightconePathActive: state.mapLightconePathActive,
mapLightconeRankActive: state.mapLightconeRankActive,
setMapLightconePathActive: state.setMapLightconePathActive,
setMapLightconeRankActive: state.setMapLightconeRankActive,
lightconeSearch: state.lightconeSearch,
setLightconeSearch: state.setLightconeSearch,
}))
)
const { setAvatar, avatars } = useUserDataStore(
useShallow((state) => ({
setAvatar: state.setAvatar,
avatars: state.avatars,
}))
)
const { setIsOpenLightcone } = useModelStore(
useShallow((state) => ({
setIsOpenLightcone: state.setIsOpenLightcone,
}))
)
const { mapLightCone, baseType } = useDetailDataStore(
useShallow((state) => ({
mapLightCone: state.mapLightCone,
baseType: state.baseType,
}))
)
const transI18n = useTranslations("DataPage")
const listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item => {
const itemRank = calcRarity(item.Rarity).toString();
return (
(allRankFalse || !!mapLightconeRankActive[itemRank] || !!mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || !!mapLightconePathActive[item.BaseType])
);
})
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<div className="space-y-4">
{/* Search & Filters */}
<div className="p-3 rounded-2xl bg-base-200 border border-base-300 space-y-2.5">
{/* Search Bar */}
<label className="input input-sm input-bordered flex items-center gap-2 bg-base-100 border-base-300 rounded-lg w-full h-8 px-2.5 shadow-xs">
<Search className="w-4 h-4 text-base-content/60 shrink-0" />
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text"
placeholder={`${transI18n("lightcones")}...`}
className="grow text-xs bg-transparent border-none outline-none focus:outline-none"
/>
{lightconeSearch && (
<button
type="button"
onClick={() => setLightconeSearch("")}
className="text-base-content/40 hover:text-base-content"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</label>
{/* Filter Chips */}
<div className="flex flex-wrap items-center justify-between gap-2">
{/* Path Filter Chips */}
<div className="flex flex-wrap items-center gap-1">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapLightconePathActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
}}
className={`h-7 w-7 rounded-lg transition-all duration-150 active-press border flex items-center justify-center ${
isActive
? "bg-primary/20 border-primary ring-1.5 ring-primary/50 shadow-xs scale-105"
: "bg-base-200 hover:bg-base-300 border-base-content/10 opacity-80 hover:opacity-100"
}`}
title={key}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className={`w-4.5 h-4.5 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={20}
height={20}
/>
</button>
);
})}
</div>
{/* Rarity Filter Chips */}
<div className="flex items-center gap-1.5">
{Object.keys(mapLightconeRankActive).map((key) => {
const isActive = !!mapLightconeRankActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
}}
className={`px-3 py-1 rounded-xl text-xs font-mono font-bold transition-all duration-200 active-press border ${
isActive
? "bg-primary text-primary-content border-primary shadow-xs scale-105"
: "bg-base-100 border-base-300 text-base-content/70 hover:text-base-content hover:bg-base-200"
}`}
>
{key}★
</button>
);
})}
</div>
</div>
</div>
{/* Results Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 gap-3 auto-rows-fr max-h-[58vh] overflow-y-auto pr-1">
{listLightcone.map((item) => (
<div
key={item.ID}
className="h-full flex flex-col"
onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected.ID.toString()]
if (avatar) {
const profileIndex = avatar.profileSelect
const currentProfile = avatar.profileList?.[profileIndex]
if (currentProfile) {
const newProfileList = [...avatar.profileList]
newProfileList[profileIndex] = {
...currentProfile,
lightcone: {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
}
setAvatar({
...avatar,
profileList: newProfileList
})
setIsOpenLightcone(false)
}
}
}
}}
>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
}
+33 -10
View File
@@ -8,14 +8,30 @@ import Image from "next/image";
import { ASEvent, MonsterStore } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function AsBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
as_config,
setAsConfig
} = useUserDataStore()
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
useShallow((state) => ({
as_config: state.as_config,
setAsConfig: state.setAsConfig,
}))
)
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
useShallow((state) => ({
mapMonster: state.mapMonster,
mapAS: state.mapAS,
damageType: state.damageType,
hardLevelConfig: state.hardLevelConfig,
eliteConfig: state.eliteConfig,
}))
)
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
@@ -108,6 +124,13 @@ export default function AsBar() {
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
if (targetEventList[0].Invasion) {
newBattleConfig.blessings.push({
id: targetEventList[0].Invasion.MazeBuff.ID,
level: 1
})
}
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -135,10 +158,10 @@ export default function AsBar() {
if (!mapAS) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="rounded-xl p-4 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
@@ -209,13 +232,13 @@ export default function AsBar() {
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
@@ -225,7 +248,7 @@ export default function AsBar() {
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
@@ -246,7 +269,7 @@ export default function AsBar() {
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<div key={i} className="rounded-xl p-4 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
+75 -69
View File
@@ -17,19 +17,37 @@ import { useTranslations } from "next-intl";
import useGlobalStore from "@/stores/globalStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { MonsterDetail } from "@/types";
import { useShallow } from "zustand/react/shallow";
export default function CeBar() {
const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { ce_config, setCeConfig } = useUserDataStore()
const { mapMonster, stage, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const { ce_config, setCeConfig } = useUserDataStore(
useShallow((state) => ({
ce_config: state.ce_config,
setCeConfig: state.setCeConfig,
}))
)
const { mapMonster, stage, damageType } = useDetailDataStore(
useShallow((state) => ({
mapMonster: state.mapMonster,
stage: state.stage,
damageType: state.damageType,
}))
)
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const transI18n = useTranslations("DataPage")
const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const { extraData, setExtraData } = useGlobalStore()
const { extraData, setExtraData } = useGlobalStore(
useShallow((state) => ({
extraData: state.extraData,
setExtraData: state.setExtraData,
}))
)
const pageSize = 30
@@ -59,10 +77,6 @@ export default function CeBar() {
[monsterPage, filteredMonsters]
)
useEffect(() => {
setMonsterPage(1)
}, [searchTerm])
const stageList = useMemo(() => Object.values(stage).map((item) => ({
id: item.ID.toString(),
name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
@@ -79,11 +93,6 @@ export default function CeBar() {
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
useEffect(() => {
if (!ce_config) return
if (!extraData || !extraData.theory_craft?.mode) return
@@ -110,27 +119,26 @@ export default function CeBar() {
return (
<div className="z-4 py-8 h-full w-full" onClick={() => {
<div className="z-4 pt-4 pb-2 h-full w-full space-y-4" onClick={() => {
setShowSearchWaveId(null)
setShowSearchStage(false)
}}>
<div className="mb-4 w-full relative">
<div className="w-full relative">
<div className="flex items-center justify-center gap-2">
<button
className="btn btn-outline w-full text-left flex items-center gap-2"
className="btn btn-outline btn-warning w-full text-left flex items-center gap-2.5 rounded-xl shadow-xs"
onClick={(e) => {
e.stopPropagation()
setShowSearchStage(!showSearchStage)
}}
>
<Search className="w-6 h-6" />
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
<Search className="w-4 h-4" />
<span className="text-left font-medium"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</button>
</div>
{showSearchStage && (
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-base-content/20 rounded-xl p-4 shadow-xl">
<div className="flex items-center gap-2 mb-2">
<label className="input w-full">
@@ -147,46 +155,44 @@ export default function CeBar() {
</g>
</svg>
<input
type="search" className="grow"
placeholder={transI18n("searchStage")}
value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)}
autoFocus
onChange={(e) => {
setStageSearchTerm(e.target.value)
setStagePage(1)
}}
type="search"
required
placeholder={transI18n("searchStage")}
/>
</label>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[48vh] overflow-y-auto pr-1">
{paginatedStages.length > 0 ? (
<>
{paginatedStages.map((stage) => (
<div
key={stage.id}
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
onClick={() => {
if (ce_config.stage_id !== Number(stage.id)) {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
}
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
{stage.name}
</div>
))}
</>
paginatedStages.map((stage) => (
<button
key={stage.id}
className="btn btn-sm btn-outline justify-start"
onClick={(e) => {
e.stopPropagation()
if (ce_config.stage_id !== Number(stage.id)) {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
}
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
<span className="truncate">{stage.name}</span>
</button>
))
) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)}
</div>
{paginatedStages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-4">
<button
disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2"
className="btn btn-sm btn-outline btn-success"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage - 1)
@@ -197,7 +203,7 @@ export default function CeBar() {
<button
disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2"
className="btn btn-sm btn-outline btn-success"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage + 1)
@@ -211,12 +217,12 @@ export default function CeBar() {
)}
</div>
<div className="relative max-w-7xl mx-auto space-y-6">
<div className="relative w-full space-y-4">
{ce_config.monsters.map((wave, waveIndex) => (
<div key={waveIndex} className="card border border-slate-700/50 ">
<div className="card-body p-6">
<div key={waveIndex} className="rounded-xl border border-warning p-4 bg-base-100">
<div className="w-full">
<div className="flex items-center flex-wrap justify-between mb-4">
<h2 className="text-xl font-bold text-white">{transI18n("wave")} {waveIndex + 1}</h2>
<h2 className="text-xl font-bold text-info">{transI18n("wave")} {waveIndex + 1}</h2>
<div className="flex gap-2">
<button
disabled={waveIndex === 0}
@@ -430,7 +436,10 @@ export default function CeBar() {
type="search" className="grow"
placeholder={transI18n("searchMonster")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={(e) => {
setSearchTerm(e.target.value)
setMonsterPage(1)
}}
autoFocus
/>
@@ -513,20 +522,17 @@ export default function CeBar() {
))}
{/* Add New Wave Button */}
<div className="card border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-primary btn-lg w-full">
<Plus className="w-6 h-6 mr-2" />
{transI18n("addNewWave")}
</button>
</div>
</div>
<button
onClick={() => {
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-outline btn-warning w-full flex items-center justify-center gap-2 rounded-xl shadow-xs font-semibold"
>
<Plus className="w-4 h-4" />
<span>{transI18n("addNewWave")}</span>
</button>
</div>
</div>
);
+49 -33
View File
@@ -6,9 +6,15 @@ import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
import PeakBar from "./peak";
import { useShallow } from "zustand/react/shallow";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
const { battle_type, setBattleType } = useUserDataStore(
useShallow((state) => ({
battle_type: state.battle_type,
setBattleType: state.setBattleType,
}))
)
const transI18n = useTranslations("DataPage")
const navItems = [
@@ -23,9 +29,9 @@ export default function MonsterBar() {
return (
<div className="min-h-screen">
<div className="w-full">
{/* Header Navigation */}
<nav className="border-b border-warning/30 relative pb-2">
<nav className="border-b border-base-content/10 relative pb-3">
<div className="flex items-center justify-center">
{/* Mobile Select */}
<div className="block md:hidden w-full">
@@ -43,38 +49,48 @@ export default function MonsterBar() {
</div>
{/* Desktop Tabs */}
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
{navItems.map((item) => (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-2 w-full">
{navItems.map((item) => {
const isSelected = battle_type.toUpperCase() === item.value.toUpperCase()
return (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2.5 rounded-xl transition-all cursor-pointer duration-300 flex items-center justify-center space-x-2 text-sm font-semibold border ${
isSelected
? 'bg-success/20 border-warning shadow-lg text-base-content'
: 'bg-base-200/20 hover:bg-base-200/40 border-transparent text-base-content/80'
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
/>
</span>
<span>{item.name}</span>
</button>
))}
<span
className={
isSelected
? ""
: "theme-path-icon"
}
style={
isSelected
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
className="w-5 h-5 object-contain"
/>
</span>
<span>{item.name}</span>
</button>
)
})}
</div>
</div>
</nav>
+34 -10
View File
@@ -9,14 +9,30 @@ import Image from "next/image";
import { useTranslations } from "next-intl";
import { MoCEvent, MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function MocBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
useShallow((state) => ({
moc_config: state.moc_config,
setMocConfig: state.setMocConfig,
}))
)
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
useShallow((state) => ({
mapMonster: state.mapMonster,
mapMoc: state.mapMoc,
damageType: state.damageType,
hardLevelConfig: state.hardLevelConfig,
eliteConfig: state.eliteConfig,
}))
)
const transI18n = useTranslations("DataPage")
@@ -86,6 +102,14 @@ export default function MocBar() {
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
if (targetEventList[0].Invasion) {
newBattleConfig.blessings.push({
id: targetEventList[0].Invasion.MazeBuff.ID,
level: 1
})
}
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -115,10 +139,10 @@ export default function MocBar() {
if (!mapMoc) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="rounded-xl p-4 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
@@ -197,8 +221,8 @@ export default function MocBar() {
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<div className="pt-3 border-t border-base-content/10 space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
@@ -215,7 +239,7 @@ export default function MocBar() {
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
@@ -225,7 +249,7 @@ export default function MocBar() {
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
@@ -246,7 +270,7 @@ export default function MocBar() {
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<div key={i} className="rounded-xl p-4 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
+378 -286
View File
@@ -1,308 +1,400 @@
"use client"
"use client";
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";;
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PeakBar() {
const { locale } = useLocaleStore()
const {
peak_config,
setPeakConfig
} = useUserDataStore()
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale })),
);
const { peak_config, setPeakConfig } = useUserDataStore(
useShallow((state) => ({
peak_config: state.peak_config,
setPeakConfig: state.setPeakConfig,
})),
);
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } =
useDetailDataStore(
useShallow((state) => ({
mapMonster: state.mapMonster,
mapPeak: state.mapPeak,
damageType: state.damageType,
eliteConfig: state.eliteConfig,
hardLevelConfig: state.hardLevelConfig,
})),
);
const transI18n = useTranslations("DataPage");
const listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()]
if (!peak) return []
const listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()];
if (!peak) return [];
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
}, [peak_config, mapPeak])
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]
}, [peak_config, mapPeak])
return [...peak.PreLevel, peak.BossLevel].filter((it) => it != null);
}, [peak_config, mapPeak]);
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()];
}, [peak_config, mapPeak]);
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak])
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak]);
const challengeSelected = useMemo(() => {
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
if (
challenge
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
&& bossConfig
&& peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true }
const challengeSelected = useMemo(() => {
const challenge = structuredClone(
listFloor?.find((peak) => peak?.ID === peak_config.challenge_id),
);
if (
challenge &&
challenge.ID ===
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID &&
bossConfig &&
peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true };
}
return {
challenge: challenge,
isBoss:
challenge?.ID ===
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
};
}, [peak_config, listFloor, mapPeak, bossConfig]);
useEffect(() => {
if (!challengeSelected.challenge) return;
if (
peak_config.event_id !== 0 &&
peak_config.challenge_id !== 0 &&
challengeSelected
) {
const newBattleConfig = structuredClone(peak_config);
newBattleConfig.cycle_count = 6;
newBattleConfig.blessings = [];
for (const value of challengeSelected?.challenge?.MazeBuff) {
newBattleConfig.blessings.push({
id: value.ID,
level: 1,
});
}
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
newBattleConfig.blessings.push({
id: peak_config.buff_id,
level: 1,
});
}
newBattleConfig.monsters = [];
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID;
if (challengeSelected.challenge.EventList[0].Invasion) {
console.log("Adding Invasion Buff", challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID);
newBattleConfig.blessings.push({
id: challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID,
level: 1,
});
}
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
if (!wave) continue;
const newWave: MonsterStore[] = [];
for (const value of Object.values(wave)) {
if (!value) continue;
newWave.push({
monster_id: value,
level: challengeSelected.challenge.EventList[0].Level,
amount: 1,
});
}
return {
challenge: challenge,
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
}
}, [peak_config, listFloor, mapPeak, bossConfig])
newBattleConfig.monsters.push(newWave);
}
useEffect(() => {
if (!challengeSelected.challenge) return
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
const newBattleConfig = structuredClone(peak_config)
newBattleConfig.cycle_count = 6
newBattleConfig.blessings = []
for (const value of challengeSelected?.challenge?.MazeBuff) {
newBattleConfig.blessings.push({
id: value.ID,
level: 1
})
setPeakConfig(newBattleConfig);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
peak_config.event_id,
peak_config.challenge_id,
peak_config.buff_id,
peak_config.boss_mode,
mapPeak,
]);
if (!mapPeak) return null;
return (
<div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */}
<div className="rounded-xl p-4 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapPeak)
.sort((a, b) => b.ID - a.ID)
.map((peak) => ({
id: peak.ID.toString(),
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
}))}
excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectPEAKEvent")}
setSelectedCustomSet={(id) =>
setPeakConfig({
...peak_config,
event_id: Number(id),
challenge_id: 0,
buff_id: 0,
})
}
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
newBattleConfig.blessings.push({
id: peak_config.buff_id,
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
if (!wave) continue
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
if (!value) continue
newWave.push({
monster_id: value,
level: challengeSelected.challenge.EventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
setPeakConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
peak_config.event_id,
peak_config.challenge_id,
peak_config.buff_id,
peak_config.boss_mode,
mapPeak,
])
if (!mapPeak) return null
return (
<div className="py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({
id: peak.ID.toString(),
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
}))}
excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectPEAKEvent")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className={
`grid grid-cols-1
/>
</div>
{/* Settings */}
<div
className={`grid grid-cols-1
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
gap-4 mb-2 justify-items-center items-center w-full`}
>
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">
{transI18n("floor")}:{" "}
</span>
</label>
<select
value={peak_config.challenge_id}
className="select select-success w-full"
onChange={(e) =>
setPeakConfig({
...peak_config,
challenge_id: Number(e.target.value),
})
}
>
<option value={0} disabled={true}>
{transI18n("selectFloor")}
</option>
{listFloor.map((peak) => (
<option key={peak.ID} value={peak.ID}>
{getLocaleName(locale, peak.Name)}
</option>
))}
</select>
</div>
{eventSelected &&
eventSelected.BossLevel?.ID === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">
{transI18n("mode")}:{" "}
</span>
</label>
<select
value={peak_config.boss_mode}
className="select select-success w-full"
onChange={(e) =>
setPeakConfig({ ...peak_config, boss_mode: e.target.value })
}
>
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={peak_config.challenge_id}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{listFloor.map((peak) => (
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option>
))}
</select>
</div>
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
</label>
<select
value={peak_config.boss_mode}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option>
</select>
</div>
)}
</div>
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div>
{
eventSelected
&& eventSelected.BossLevel?.ID === peak_config.challenge_id
&& bossConfig
&& (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
bossConfig.BuffList.map((buff) => ({
id: buff.ID.toString(),
name: getLocaleName(locale, buff?.Name || ""),
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []),
}))
}
excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
/>
</div>
)
}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">
{transI18n("turbulenceBuff")}
</h2>
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">
{index + 1}. {getLocaleName(locale, subOption.Name)}
</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc),
subOption.Param || []
)
}}
/>
</div>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(peak_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 gap-4">
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2>
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="flex flex-wrap gap-2 mt-2">
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[monsterId.toString()],
waveValue.EliteGroup,
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0,
challengeSelected?.challenge?.EventList?.[0]?.Level || 0,
hardLevelConfig,
eliteConfig
);
return (
<div
key={enemyIndex}
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
>
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
alt="Enemy Icon"
width={150}
height={150}
className="w-full h-full object-cover"
/>
</div>
)}
</div>
<div className="flex flex-col px-1 pb-2 pt-2">
<div className="flex flex-col space-y-1.5">
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-error">HP</span>
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-info">Speed</span>
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
Weakness
</span>
<div className="flex items-center justify-center gap-1.5 flex-wrap">
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
<Image
key={iconIndex}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
alt={icon}
width={40}
height={40}
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
/>
))}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
<option value={0} disabled={true}>
{transI18n("selectSide")}
</option>
<option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option>
</select>
</div>
)}
</div>
)
}
<div className="label-text font-bold text-success mb-2">
StageId: {peak_config?.stage_id}
</div>
{eventSelected &&
eventSelected.BossLevel?.ID === peak_config.challenge_id &&
bossConfig && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={bossConfig.BuffList.map((buff) => ({
id: buff.ID.toString(),
name: getLocaleName(locale, buff?.Name || ""),
description: replaceByParam(
getLocaleName(locale, buff?.Desc || ""),
buff?.Param || [],
),
}))}
excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) =>
setPeakConfig({ ...peak_config, buff_id: Number(id) })
}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">
{transI18n("turbulenceBuff")}
</h2>
{challengeSelected?.challenge &&
challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
<div key={index} className="space-y-1">
<span className="font-bold text-success text-sm">
{index + 1}. {getLocaleName(locale, subOption.Name)}
</span>
<div
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc),
subOption.Param || [],
),
}}
/>
</div>
))
) : (
<div className="text-sm text-base-content/50 italic">
{transI18n("noTurbulenceBuff")}
</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(peak_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 gap-4">
<div className="rounded-xl p-4 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">
{getLocaleName(locale, challengeSelected?.challenge?.Name)}
</h2>
{challengeSelected?.challenge &&
Object.values(
challengeSelected?.challenge?.EventList?.[0]?.Infinite || [],
).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold">
{transI18n("wave")} {waveIndex + 1}
</h3>
<div className="flex flex-wrap gap-2 mt-2">
{Array.from(new Set(waveValue.MonsterList)).map(
(monsterId, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[monsterId.toString()],
waveValue.EliteGroup,
challengeSelected?.challenge?.EventList?.[0]
?.HardLevelGroup || 0,
challengeSelected?.challenge?.EventList?.[0]?.Level ||
0,
hardLevelConfig,
eliteConfig,
);
return (
<div
key={enemyIndex}
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
>
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
Lv.{" "}
{
challengeSelected?.challenge?.EventList?.[0]
?.Level
}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[monsterId.toString()]?.Image
?.IconPath && (
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
alt="Enemy Icon"
width={150}
height={150}
className="w-full h-full object-cover"
/>
</div>
)}
</div>
<div className="flex flex-col px-1 pb-2 pt-2">
<div className="flex flex-col space-y-1.5">
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-error">
HP
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.hp.toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-info">
Speed
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.spd.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-base-content/70">
Toughness
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.stance.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
Weakness
</span>
<div className="flex items-center justify-center gap-1.5 flex-wrap">
{mapMonster?.[
monsterId.toString()
]?.StanceWeakList?.map((icon, iconIndex) => (
<Image
key={iconIndex}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
alt={icon}
width={40}
height={40}
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
/>
))}
</div>
</div>
</div>
</div>
);
},
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
+37 -16
View File
@@ -8,14 +8,30 @@ import Image from "next/image";
import { MonsterStore, PFEvent } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PfBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
useShallow((state) => ({
pf_config: state.pf_config,
setPfConfig: state.setPfConfig,
}))
)
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
useShallow((state) => ({
mapMonster: state.mapMonster,
mapPF: state.mapPF,
damageType: state.damageType,
hardLevelConfig: state.hardLevelConfig,
eliteConfig: state.eliteConfig,
}))
)
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
@@ -89,6 +105,14 @@ export default function PfBar() {
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
if (targetEventList[0].Invasion) {
newBattleConfig.blessings.push({
id: targetEventList[0].Invasion.MazeBuff.ID,
level: 1
})
}
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -116,10 +140,10 @@ export default function PfBar() {
if (!mapPF) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="rounded-xl p-4 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
@@ -190,16 +214,14 @@ export default function PfBar() {
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
</label>
<div key={index} className="space-y-1">
<span className="font-bold text-success text-sm">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
<div
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc) || "",
@@ -213,7 +235,7 @@ export default function PfBar() {
challengeSelected?.MazeBuff?.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
@@ -223,13 +245,12 @@ export default function PfBar() {
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(pf_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
@@ -245,7 +266,7 @@ export default function PfBar() {
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<div key={i} className="rounded-xl p-4 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
+15 -15
View File
@@ -1,16 +1,16 @@
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
}
+11 -11
View File
@@ -1,12 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
+505 -472
View File
@@ -1,473 +1,506 @@
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
export default function QuickView() {
const { avatars } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { avatarSelected, } = useCurrentDataStore()
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: "Elation Boost",
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt="Stat Icon"
width={40}
height={40}
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
/>
<div className="font-bold">{stat.name}</div>
</div>
<div className="ml-3 mr-3 grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
<hr />
</div>
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
<div>
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="flex flex-col items-center justify-center">
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
</div>
</div>
)}
</div>
</div>
)
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
import { useShallow } from "zustand/react/shallow";
export default function QuickView() {
const { avatars } = useUserDataStore(
useShallow((state) => ({ avatars: state.avatars }))
)
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
)
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore(
useShallow((state) => ({
mainAffix: state.mainAffix,
subAffix: state.subAffix,
mapRelicSet: state.mapRelicSet,
mapLightCone: state.mapLightCone,
mapAvatar: state.mapAvatar,
}))
)
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect] ?? avatarData?.profileList?.[0]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
const profile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
if (profile?.relics) {
for (const relic of Object.values(profile.relics)) {
if (relic && relic.relic_set_id) {
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: transI18n("hp"),
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: transI18n("atk"),
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: transI18n("def"),
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: transI18n("speed"),
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: transI18n("critRate"),
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: transI18n("critDmg"),
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: transI18n("breakEffect"),
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: transI18n("effectRes"),
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: transI18n("energyRegenerationRate"),
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: transI18n("effectHitRate"),
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: transI18n("outgoingHealingBoost"),
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: transI18n("physicalDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: transI18n("fireDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: transI18n("iceDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: transI18n("thunderDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: transI18n("windDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: transI18n("quantumDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: transI18n("imaginaryDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: transI18n("elation"),
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-1">
{/* Stats Overview Column - Clean Minimal Grid, No Box-in-Box */}
<div className="lg:col-span-6 space-y-6">
<div className="space-y-3">
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
<div className="flex items-center gap-2">
<span className="w-1 h-3.5 bg-primary rounded-full" />
<h3 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
{transI18n("character")}
</h3>
</div>
<span className="text-xs font-mono font-medium text-base-content/60">
{transI18n("level")} {avatarData?.level || 1}
</span>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div
key={index}
className="flex items-center justify-between py-1.5 border-b border-base-content/5"
>
<div className="flex items-center gap-2 min-w-0 pr-2">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt={stat.name}
width={20}
height={20}
className="h-5 w-5 theme-stat-icon shrink-0 object-contain"
/>
<span className="font-medium text-base-content/80 truncate">{stat.name}</span>
</div>
<span className="font-mono font-semibold text-primary shrink-0 text-right">
{stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0}
<span className="text-[10px] font-normal opacity-80 ml-0.5">{stat.unit}</span>
</span>
</div>
)
})}
</div>
</div>
{/* Set Effects - Flat Minimalist Row */}
{relicEffects.length > 0 && (
<div className="space-y-2 pt-2">
<div className="flex items-center gap-2 pb-1 border-b border-base-content/10">
<span className="w-1 h-3.5 bg-amber-500 rounded-full" />
<h4 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
{transI18n("relics")}
</h4>
</div>
<div className="space-y-1">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div
key={index}
className="flex items-center justify-between text-xs py-1.5 border-b border-base-content/5"
>
<div
className="font-medium text-base-content/90 truncate max-w-[80%]"
dangerouslySetInnerHTML={{
__html: replaceByParam(getLocaleName(locale, relicInfo.Name), [])
}}
/>
<span className="font-mono font-semibold text-amber-600 dark:text-amber-400 text-xs shrink-0">
{setEffect.count} Pc
</span>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Relic Equipment Cards Column */}
<div className="lg:col-span-6 space-y-2">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="p-8 text-center text-base-content/40 text-xs italic">
{transI18n("noRelicEquipped")}
</div>
)}
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+434 -366
View File
@@ -1,366 +1,434 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore()
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = useRelicMakerStore()
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore()
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore()
const { mapRelicSet, subAffix } = useDetailDataStore()
const { locale } = useLocaleStore()
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
delete avatar.profileList[avatar.profileSelect].relics[slot]
setAvatars({ ...avatars });
}
}
const handlerChangeRelic = (slot: string) => {
const relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: "",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Section - Items Grid */}
<div className="lg:col-span-2">
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("relics")}
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div key={index} className="relative group">
<div
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
className="cursor-pointer"
>
<RelicCard
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-2 mt-10">
<button
disabled={!selectedRelicSlot}
onClick={() => {
handlerChangeRelic(selectedRelicSlot)
}}
className="btn btn-info"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button>
</div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div>
</div>
{/* Right Section - Stats and Set Effects */}
<div className="space-y-6">
{/* Set Effects Panel */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("setEffects")}
</h3>
<div className="space-y-6">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="space-y-3">
<div className="flex items-center gap-2">
<div
className="font-bold text-warning"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div>
<div className="space-y-2 pl-4">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
✕
</motion.button>
</div>
{title && (
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
)}
{content}
</div>
</dialog>
))}
</div>
);
}
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
import { useShallow } from 'zustand/react/shallow';
export default function RelicsInfo() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
)
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = useRelicMakerStore(
useShallow((state) => ({
setSelectedRelicSlot: state.setSelectedRelicSlot,
selectedRelicSlot: state.selectedRelicSlot,
setSelectedMainStat: state.setSelectedMainStat,
setSelectedRelicSet: state.setSelectedRelicSet,
setSelectedRelicLevel: state.setSelectedRelicLevel,
setListSelectedSubStats: state.setListSelectedSubStats,
resetHistory: state.resetHistory,
resetSubStat: state.resetSubStat,
listSelectedSubStats: state.listSelectedSubStats,
}))
)
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore(
useShallow((state) => ({
isOpenRelic: state.isOpenRelic,
setIsOpenRelic: state.setIsOpenRelic,
isOpenQuickView: state.isOpenQuickView,
setIsOpenQuickView: state.setIsOpenQuickView,
}))
)
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
}))
)
const { mapRelicSet, subAffix } = useDetailDataStore(
useShallow((state) => ({
mapRelicSet: state.mapRelicSet,
subAffix: state.subAffix,
}))
)
const { locale } = useLocaleStore(
useShallow((state) => ({
locale: state.locale,
}))
)
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
return profile?.relics?.[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatarKey = avatarSelected?.ID.toString() || "";
const avatar = avatars[avatarKey];
if (avatar) {
const profileIndex = avatar.profileSelect;
const currentProfile = avatar.profileList?.[profileIndex];
if (currentProfile) {
const newRelics = { ...currentProfile.relics };
delete newRelics[slot];
const newProfileList = [...avatar.profileList];
newProfileList[profileIndex] = {
...currentProfile,
relics: newRelics,
};
setAvatar({
...avatar,
profileList: newProfileList,
});
}
}
};
const handlerChangeRelic = (slot: string) => {
const relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
if (newSubAffixes[index]) {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"]?.[item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
}
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
const currentProfile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
if (currentProfile?.relics) {
for (const relic of Object.values(currentProfile.relics)) {
if (relic) {
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: transI18n("changeRelic") || "Cài đặt di vật",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="w-full space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Left Section - Relic Equipment Grid */}
<div className="lg:col-span-2 space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-6">
<div className="flex items-center justify-between pb-3 border-b border-base-content/10">
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
{transI18n("relics")}
</span>
</h2>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-sm btn-outline btn-primary rounded-xl font-bold shadow-xs"
>
{transI18n("quickView")}
</button>
</div>
{/* 6-Slot Grid completely free of box-in-box */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6 py-4">
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div
key={index}
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
className={`relative group flex flex-col items-center justify-center p-2 rounded-2xl transition-all duration-200 cursor-pointer ${
item === selectedRelicSlot
? "bg-primary/5 rounded-2xl ring-1.5 ring-primary/40"
: "hover:bg-base-content/5"
}`}
>
<RelicCard
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
{/* Micro Actions */}
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-xs btn-circle btn-primary shadow-xs"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-xs btn-circle btn-error shadow-xs"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
{/* Bottom Actions Bar */}
<div className="grid grid-cols-2 gap-3 pt-2">
<button
disabled={!selectedRelicSlot}
onClick={() => handlerChangeRelic(selectedRelicSlot)}
className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl"
>
{transI18n("deleteRelic")}
</button>
</div>
</div>
</div>
{/* Right Section - Set Effects Showcase */}
<div className="space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-4">
<div className="pb-3 border-b border-base-content/10">
<h3 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-amber-400 to-amber-600 rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-amber-400 to-amber-600 font-bold tracking-tight">
{transI18n("setEffects")}
</span>
</h3>
</div>
{relicEffects.length > 0 ? (
<div className="divide-y divide-base-content/10">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="py-3.5 first:pt-0 last:pb-0 space-y-2">
<div className="flex items-center justify-between">
<div
className="font-bold text-sm text-amber-500"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className="badge badge-primary badge-sm font-mono font-bold">
{setEffect.count} Pc
</span>
)}
</div>
<div className="space-y-2 pt-1">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="text-xs space-y-0.5 pl-2 border-l-2 border-primary/40">
<div className="font-bold text-emerald-500">
{requireNum}-PC Bonus
</div>
<div
className="text-base-content/80 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
) : (
<div className="text-center py-10 text-base-content/40 text-xs italic">
{transI18n("noRelicEquipped")}
</div>
)}
</div>
</div>
</div>
{/* Modals */}
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className={`modal-box ${id === 'quick_view_modal' ? 'w-11/12 max-w-4xl' : 'w-11/12 max-w-[92vw] sm:max-w-[88vw]'} max-h-[90vh] bg-base-100 text-base-content border border-base-content/10 shadow-2xl rounded-2xl p-0 overflow-hidden`}>
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100">
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
{title}
</h3>
<button
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
onClick={onClose}
>
✕
</button>
</div>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
{content}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog>
))}
</div>
);
}
+136 -112
View File
@@ -1,112 +1,136 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import { useShallow } from "zustand/react/shallow"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore(
useShallow((state) => ({
locale: state.locale,
theme: state.theme,
}))
)
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 14px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer',
opacity: 1,
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text,
opacity: 1,
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none',
opacity: 1,
minHeight: '38px',
borderRadius: '12px',
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 99999,
opacity: 1,
border: `1px solid ${c.border}`,
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)',
borderRadius: '12px',
overflow: 'hidden',
}),
menuList: (p: any) => ({
...p,
backgroundColor: c.bg,
padding: 0,
opacity: 1,
}),
menuPortal: (p: any) => ({
...p,
zIndex: 99999,
opacity: 1,
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
+7 -2
View File
@@ -7,6 +7,7 @@ import useLocaleStore from '@/stores/localeStore'
import { themeColors } from '@/constant/constant'
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
import { useShallow } from "zustand/react/shallow"
const Select = dynamic(
() => import("react-select").then(m => m.default),
@@ -32,7 +33,9 @@ type SelectCustomProp = {
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { theme } = useLocaleStore()
const { theme } = useLocaleStore(
useShallow((state) => ({ theme: state.theme }))
)
const c = themeColors[theme] || themeColors.winter
const customStyles = {
@@ -87,7 +90,9 @@ export default function SelectCustomText({ customSet, excludeSet, selectedCustom
)
}}
/>
{option.time && <div className='text-base'>{option.time}</div>}
{option.time && !option.time.includes("undefined") && (
<div className='text-sm opacity-70'>{option.time}</div>
)}
{option.description && <div
className="text-base"
dangerouslySetInnerHTML={{
File diff suppressed because it is too large Load Diff
+93 -107
View File
@@ -1,108 +1,94 @@
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<>
<div
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={78}
height={78}
alt="Relic Icon"
className="h-19.5 w-19.5 rounded-lg"
/>
<div
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{
left: '0.65rem',
bottom: '-0.45rem',
fontSize: '1.05rem',
letterSpacing: '-0.1em',
}}
>
✦✦✦✦✦
</div>
</div>
<div className=" flex w-1/6 flex-col items-center justify-center">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={35}
height={35}
alt="Main Affix Icon"
className="h-8.75 w-8.75"
/>
</div>
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
alt="Sub Affix Icon"
className="h-6 w-6 shrink-0"
/>
) : (
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
<span className="text-xs text-white/50">?</span>
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span>
{
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
{subAffix?.count}
</span>
)}
</div>
</div>
)
})}
</div>
</div>
</>
)
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<div className="flex items-center gap-3 py-2 px-1 border-b border-base-content/10 last:border-b-0">
{/* Relic Figure Icon */}
<div className="relative shrink-0 flex flex-col items-center">
<NextImage
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={48}
height={48}
alt="Relic"
className="h-12 w-12 object-contain"
/>
</div>
{/* Main Affix - Clean Display */}
<div className="flex w-20 shrink-0 flex-col items-start justify-center">
<div className="flex items-center gap-1">
{relic?.mainAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${relic.mainAffix.detail.icon}`}
unoptimized
crossOrigin="anonymous"
width={16}
height={16}
alt="Main Affix"
className="h-4 w-4 theme-stat-icon shrink-0 object-contain"
/>
) : null}
<span className="text-xs font-bold text-amber-600 dark:text-amber-400 font-mono">
{relic?.mainAffix?.valueAffix + (relic?.mainAffix?.detail?.unit || "")}
</span>
</div>
<span className="text-[10px] font-mono text-base-content/50 ml-0.5">
+{relic?.mainAffix?.level}
</span>
</div>
{/* Sub Affixes Grid - Clean Flat Typography */}
<div className="grid flex-1 grid-cols-2 gap-x-3 gap-y-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
const isOptimalStat = avatarInfo?.Relics?.SubAffixPropertyList?.includes(subAffix?.property)
return (
<div
key={index}
className={`flex items-center justify-between py-0.5 min-w-0 ${
isOptimalStat
? "text-amber-600 dark:text-amber-400 font-semibold"
: "text-base-content/75"
}`}
>
<div className="flex items-center gap-1 min-w-0 flex-1">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix.detail.icon}`}
unoptimized
crossOrigin="anonymous"
width={14}
height={14}
alt="Sub Affix"
className="h-3.5 w-3.5 theme-stat-icon shrink-0 object-contain"
/>
) : null}
<span className="text-xs font-mono truncate">
+{subAffix?.valueAffix + (subAffix?.detail?.unit || "")}
</span>
</div>
{isOptimalStat && subAffix?.count ? (
<span className="ml-1 text-[10px] font-bold text-amber-600/80 dark:text-amber-400/80 shrink-0 font-mono">
({subAffix?.count})
</span>
) : null}
</div>
)
})}
</div>
</div>
)
}
+402 -384
View File
@@ -1,385 +1,403 @@
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme } = useLocaleStore()
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
const { avatars, setAvatar } = useUserDataStore()
const { locale } = useLocaleStore()
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("skills")}
</h2>
<div className="flex flex-col items-center">
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
{traceButtons && avatarSelected && (
<div className="grid col-span-4 relative w-full aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt=""
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className={`w-full h-full object-cover rounded-xl`}
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null
}
return (
<div
key={`${btn.id} + ${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black
bg-no-repeat bg-contain
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110z-10
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
? "opacity-50 cursor-not-allowed"
: ""}
`}
onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
}}
/>
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
<p className="
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
font-bold text-center rounded-full absolute
translate-y-full mt-1
bg-base-300 px-1
left-1/2 transform -translate-x-1/2
">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
{btn.size === "special" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
borderRadius: "50%"
}}
/>
)}
{btn.size === "memory" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.4,
borderRadius: "50%"
}}
/>
)}
{btn.size === "elation" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
borderRadius: "50%"
}}
/>
)}
{btn.size === "big" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.3,
borderRadius: "50%"
}}
/>
)}
</div>
)
})}
</div>
)}
{!traceButtons && avatarSelected && (
<div className="flex flex-col relative w-full aspect-square">
</div>
)}
</div>
</div>
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("details")}
</h2>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
<div>
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div>
<div className="font-bold text-success">{transI18n("level")}</div>
<div className="w-full max-w-xs">
<input type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-success"
step="1" />
<div className="flex justify-between px-2.5 mt-2 text-xs">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
<span key={index}>{index}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-success"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<div className="font-bold text-success">
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</div>
</div>
) : (
null
)}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
&& (
<div className="text-xl font-bold flex items-center gap-2 mt-2">
{getLocaleName(locale, skillInfo.PointName)}
{skillInfo.StatusAddList.length > 0 && (
<div>
{skillInfo.StatusAddList.map((status, index) => (
<div key={index}>
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
</div>
))}
</div>
)}
</div>
)}
{skillInfo?.PointDesc && (
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{skillInfo?.LevelUpSkillID
&& skillInfo?.LevelUpSkillID.length > 0
&& dataLevelUpSkill
&& (
<div className="mt-2 flex flex-col gap-2">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index}>
<div className="text-xl font-bold text-primary">
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
</div>
<SkillDescription
skill={skill}
locale={locale}
avatarData={avatarData}
skillInfo={skillInfo}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
import { useShallow } from "zustand/react/shallow";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme, locale } = useLocaleStore(
useShallow((state) => ({ theme: state.theme, locale: state.locale }))
)
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
skillIDSelected: state.skillIDSelected,
setSkillIDSelected: state.setSkillIDSelected,
}))
)
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
)
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-6">
{/* Left Panel: Skill Tree Visualizer */}
<div className="lg:col-span-6 space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-base-300">
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
{transI18n("skills")}
</span>
</h2>
<button
className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs active-press"
onClick={handlerMaxAll}
>
{transI18n("maxAll")}
</button>
</div>
{/* Visual Skill Tree Container */}
<div className="flex justify-center items-center py-2">
{traceButtons && avatarSelected && (
<div className="relative w-full max-w-md aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt="Skill Tree"
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className="w-full h-full object-cover"
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null
}
const isSelected = skillIDSelected === btn.id;
const isLearned = !!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID];
const isMajor = btn.size === "big" || btn.size === "memory" || btn.size === "elation";
return (
<div
key={`${btn.id}-${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black/70 cursor-pointer transition-all duration-150 ease-out flex justify-center items-center select-none overflow-visible
${btn.size === "small" ? "w-[6.5vw] h-[6.5vw] md:w-[2.2vw] md:h-[2.2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8.5vw] h-[8.5vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "memory" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${isSelected ? "ring-3 ring-cyan-400 ring-offset-2 ring-offset-base-100 shadow-[0_0_12px_rgba(34,211,238,0.8)] z-20" : "hover:ring-2 hover:ring-primary/40 z-10"}
${!isLearned ? "opacity-40 grayscale" : "shadow-[0_0_6px_rgba(255,255,255,0.4)]"}
`}
onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: !isMajor ? "brightness(0%)" : ""
}}
/>
{/* Skill Type Color Overlays */}
{btn.size === "special" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
}}
/>
)}
{btn.size === "memory" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.45,
}}
/>
)}
{btn.size === "elation" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
}}
/>
)}
{btn.size === "big" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.35,
}}
/>
)}
{isMajor && (
<p className="z-12 text-[10px] sm:text-xs font-mono font-bold text-center rounded-full absolute translate-y-full mt-0.5 bg-black/80 text-white px-1.5 py-0.2 border border-white/20 left-1/2 -translate-x-1/2 shadow-xs whitespace-nowrap">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
</div>
)
})}
</div>
)}
</div>
</div>
</div>
{/* Right Panel: Skill Details */}
<div className="lg:col-span-6 space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-5">
<div className="pb-3 border-b border-base-300">
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
{transI18n("details")}
</span>
</h2>
</div>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData ? (
<div className="space-y-4">
{/* Level Slider */}
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div className="space-y-1.5 pb-2">
<div className="text-sm font-bold text-base-content/80">
{transI18n("level")}
</div>
<div className="w-full max-w-sm space-y-1">
<input
type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-primary range-xs w-full"
step="1"
/>
<div className="flex justify-between px-1 text-[11px] font-mono text-base-content/50">
{Array.from({ length: skillInfo?.MaxLevel || 1 }, (_, index) => index + 1).map((idx) => (
<span key={idx}>{idx}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-3 pb-2">
<span className="text-sm font-bold text-base-content/80">{transI18n("status")}</span>
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-primary toggle-sm"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<span className={`text-xs font-bold ${avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? 'text-primary' : 'text-base-content/40'}`}>
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</span>
</div>
) : null}
{/* Point Name & Stat Additions */}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) && (
<div className="space-y-1.5">
<h3 className="text-lg font-extrabold text-base-content">
{getLocaleName(locale, skillInfo.PointName)}
</h3>
{skillInfo.StatusAddList.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{skillInfo.StatusAddList.map((status, index) => (
<span key={index} className="text-xs font-mono font-bold px-2 py-0.5 rounded-lg bg-primary/10 text-primary border border-primary/20">
+{getTraceBuffDisplay(status)}
</span>
))}
</div>
)}
</div>
)}
{/* Skill Description Prose */}
{skillInfo?.PointDesc && (
<div
className="text-xs sm:text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{/* Level-up Skill Items (Major / Servant Skills) */}
{skillInfo?.LevelUpSkillID && skillInfo?.LevelUpSkillID.length > 0 && dataLevelUpSkill && (
<div className="space-y-6 pt-1">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index} className={index > 0 ? "border-t border-base-content/10 pt-4" : ""}>
<SkillDescription
skill={skill}
locale={locale}
avatarData={avatarData}
skillInfo={skillInfo}
attackTypeLabel={transI18n(dataLevelUpSkill.isServant ? (skill?.AttackType ? "severaltalent" : "servantskill") : (skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"))}
skillEffectLabel={skill?.SkillEffect ? transI18n(skill.SkillEffect.toLowerCase()) : undefined}
/>
</div>
))}
</div>
)}
</div>
) : (
<div className="text-center py-16 text-base-content/40 text-sm italic">
Select a trace node from the skill tree to configure
</div>
)}
</div>
</div>
</div>
</div>
);
}
+57 -40
View File
@@ -1,41 +1,58 @@
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
return (
<div className="flex flex-col gap-2">
<div className="space-y-2 pb-2">
<div className="flex items-center gap-2">
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
<div
className="text-lg font-bold tracking-wide text-foreground uppercase"
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
/>
</div>
<div
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
</div>
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo, attackTypeLabel, skillEffectLabel }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint,
attackTypeLabel?: string,
skillEffectLabel?: string
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra || {}).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
const skillName = getLocaleName(locale, skill.Name);
return (
<div className="space-y-2.5">
{/* Skill Name Heading */}
<h3 className="text-xl font-bold text-base-content tracking-tight">
{skillName}
</h3>
{/* Attack Type & Effect Tag */}
{(attackTypeLabel || skillEffectLabel) && (
<div className="text-sm font-bold text-primary flex items-center gap-1.5">
<span>{attackTypeLabel}</span>
{skillEffectLabel && (
<span className="text-primary/70 font-medium">({skillEffectLabel})</span>
)}
</div>
)}
{/* Skill Subtitle with accent bar */}
<div className="flex items-center gap-2 text-sm font-bold text-primary uppercase tracking-wide">
<span className="w-1 h-3.5 bg-primary rounded-xs"></span>
<span>{skillName}</span>
</div>
{/* Description */}
<div
className="text-xs sm:text-sm leading-relaxed text-base-content/85"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
{/* Extra Effects Accordion */}
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
};
@@ -1,8 +1,8 @@
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./clientThemeWrapper"
export * from "./themeContext"
export * from "./clientThemeWrapper"
export * from "./themeContext"
+32 -40
View File
@@ -1,41 +1,33 @@
"use client";
import { createContext, PropsWithChildren, useEffect } from "react";
import useLocaleStore from "@/stores/localeStore";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore()
useEffect(() => {
if (typeof window !== "undefined") {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) setTheme(storedTheme);
}
}, [setTheme]);
const changeTheme = (nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme);
}
} else {
setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
}
};
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
"use client";
import { createContext, PropsWithChildren, useCallback } from "react";
import useLocaleStore from "@/stores/localeStore";
import { useShallow } from "zustand/react/shallow";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore(
useShallow((state) => ({
theme: state.theme,
setTheme: state.setTheme,
}))
);
const changeTheme = useCallback((nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
} else {
setTheme(theme === "winter" ? "night" : "winter");
}
}, [theme, setTheme]);
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
};
+220 -220
View File
@@ -1,220 +1,220 @@
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
+1147 -1147
View File
File diff suppressed because it is too large Load Diff
+104 -104
View File
@@ -1,105 +1,105 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
}
+14 -14
View File
@@ -1,15 +1,15 @@
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}
+119 -119
View File
@@ -1,89 +1,89 @@
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
Object.entries(avatars ?? {}).forEach(([avatarId, avatar]) => {
if (!avatar) return
@@ -91,10 +91,10 @@ export function converterToFreeSRJson(
for (const [skillId, level] of Object.entries(avatar?.data?.skills || {})) {
if (skillConfig?.[skillId]) {
skillsByAnchorType[skillConfig[skillId].IndexSlot] = level > skillConfig[skillId].MaxLevel ? skillConfig[skillId].MaxLevel : level
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0),
data: {
rank: Number(avatar.data?.rank || 0),
@@ -115,21 +115,21 @@ export function converterToFreeSRJson(
const newLightcone: LightconeJson = {
level: Number(currentProfile.lightcone.level || 0),
item_id: Number(currentProfile.lightcone.item_id || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
relic_id: Number(relic.relic_id || 0),
relic_set_id: Number(relic.relic_set_id || 0),
main_affix_id: Number(relic.main_affix_id || 0),
@@ -137,18 +137,18 @@ export function converterToFreeSRJson(
internal_uid: internalUidRelic,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
}
+61 -61
View File
@@ -1,62 +1,62 @@
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
}
+19 -19
View File
@@ -1,23 +1,23 @@
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
return Object.values(avatarSelected?.SkillTrees ?? {}).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>);
}
}
return acc;
}, {} as Record<string, number>);
}
+10 -8
View File
@@ -1,8 +1,10 @@
export * from "./getName"
export * from "./replaceByParam"
export * from "./converterToAvatarStore"
export * from "./calcData"
export * from "./random"
export * from "./json"
export * from "./convertData"
export * from "./connect"
export * from "./getName";
export * from "./replaceByParam";
export * from "./converterToAvatarStore";
export * from "./converterToFreeSRJson";
export * from "./getSkillTree";
export * from "./calcData";
export * from "./random";
export * from "./json";
export * from "./convertData";
export * from "./connect";
+23 -23
View File
@@ -8,27 +8,27 @@ export function randomPartition(sum: number, parts: number): number[] {
const raw = Array.from({ length: parts }, () => Math.random());
const total = raw.reduce((a, b) => a + b, 0);
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
}
+34 -34
View File
@@ -1,39 +1,39 @@
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
export function replaceByParam(desc?: string, params: number[] = []): string {
if (!desc) return "";
const PARAM_REGEX = /#(\d+)\[(f(\d+)|i)\](%)?/g;
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
}
+1 -1
View File
@@ -1 +1 @@
export * from "./useChangeTheme";
export * from "./useChangeTheme";
+3 -3
View File
@@ -1,4 +1,4 @@
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
export const useChangeTheme = () => useContext(ThemeContext);
+140 -141
View File
@@ -1,142 +1,141 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: any }) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch changelog:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: unknown) {
return (error as Error)?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: Record<string, unknown> | unknown }) {
try {
const response = await axios.post(`/api/proxy`, typeof data === "object" && data !== null ? { ...data } : data)
return response.data;
} catch (error: unknown) {
return error;
}
}
+37 -37
View File
@@ -1,38 +1,38 @@
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
}
+9 -9
View File
@@ -1,10 +1,10 @@
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchChangelog";
+29 -26
View File
@@ -1,26 +1,29 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore()
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore(
useShallow((state) => ({ setMapAS: state.setMapAS }))
)
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
+57 -42
View File
@@ -1,43 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected, setAvatarSelected } = useCurrentDataStore()
const query = useQuery({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
for (const avatar of Object.values(avatarStore)) {
setAvatar(avatar)
}
}, [mapAvatar])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
import { useShallow } from 'zustand/react/shallow'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore(
useShallow((state) => ({
setMapAvatar: state.setMapAvatar,
mapAvatar: state.mapAvatar,
}))
)
const { setAvatars, avatars } = useUserDataStore(
useShallow((state) => ({
setAvatars: state.setAvatars,
avatars: state.avatars,
}))
)
const { avatarSelected, setAvatarSelected } = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
setAvatarSelected: state.setAvatarSelected,
}))
)
const query = useQuery({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (!mapAvatar || Object.keys(mapAvatar).length === 0) return
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
if (listAvatarNotExist.length === 0) return
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
setAvatars({ ...avatars, ...avatarStore })
}, [mapAvatar, avatars, setAvatars])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
}
+41 -32
View File
@@ -1,32 +1,41 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
const { setIsChangelog } = useModelStore()
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore(
useShallow((state) => ({
currentVersion: state.currentVersion,
setChangelog: state.setChangelog,
setCurrentVersion: state.setCurrentVersion,
}))
)
const { setIsChangelog } = useModelStore(
useShallow((state) => ({ setIsChangelog: state.setIsChangelog }))
)
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore()
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore(
useShallow((state) => ({ setMapLightCone: state.setMapLightCone }))
)
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore()
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore(
useShallow((state) => ({ setMapMoc: state.setMapMoc }))
)
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
+38 -33
View File
@@ -1,33 +1,38 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore()
const { setSettingData } = useCurrentDataStore()
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore(
useShallow((state) => ({ setMetaData: state.setMetaData }))
)
const { setSettingData } = useCurrentDataStore(
useShallow((state) => ({ setSettingData: state.setSettingData }))
)
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore()
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore(
useShallow((state) => ({ setMapMonster: state.setMapMonster }))
)
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore()
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore(
useShallow((state) => ({ setMapPeak: state.setMapPeak }))
)
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore()
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore(
useShallow((state) => ({ setMapPF: state.setMapPF }))
)
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore()
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore(
useShallow((state) => ({ setMapRelicSet: state.setMapRelicSet }))
)
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}

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