Refactor TypeScript interfaces and types across multiple files for consistency and clarity
Gitea Auto Deploy / Deploy-Container (push) Failing after 46s

- Updated metadata interfaces in `metaData.ts` for improved readability.
- Refactored `mics.ts` types to enhance structure and maintainability.
- Added `Invasion` type to `mocDetail.ts` and `peakDetail.ts` for new functionality.
- Simplified modal configuration in `modelConfig.ts`.
- Cleaned up monster detail interfaces in `monsterDetail.ts`.
- Enhanced battle configuration types in `pfDetail.ts` to include `Invasion`.
- Updated JSON schemas in `srtools.zod.ts` to use `nullish` for optional fields.
- Introduced new skills-lock.json for skill management.
- Added global type declarations for CSS and SCSS modules in `global.d.ts`.
This commit is contained in:
2026-08-27 00:41:35 +07:00
parent 7699ac7139
commit 73b6fc268f
133 changed files with 16116 additions and 14971 deletions
+1
View File
@@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
.agents/
.history/ .history/
/node_modules /node_modules
/.pnp /.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.
+13 -5
View File
@@ -1,18 +1,26 @@
[ [
{ {
"version": "4.4.0", "version": "4.4.5",
"date": "15/07/2026", "date": "16/07/2026",
"type": "update", "type": "update",
"items": [ "items": [
"New data for 4.4.0" "New data for 4.4.5"
] ]
}, },
{ {
"version": "4.3.0", "version": "4.3.5",
"date": "02/06/2026",
"type": "update",
"items": [
"New data for 4.3.5"
]
},
{
"version": "4.2.5",
"date": "01/05/2026", "date": "01/05/2026",
"type": "update", "type": "update",
"items": [ "items": [
"New data for 4.3.0" "New data for 4.2.5"
] ]
}, },
{ {
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/**", "node_modules/**",
".next/**", ".next/**",
".history/**", ".history/**",
".agents/**",
"out/**", "out/**",
"build/**", "build/**",
"next-env.d.ts", "next-env.d.ts",
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly-Tools von Firefly Shelter" "description": "Firefly-Tools von Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Fähigkeitstyp", "skillType": "Fähigkeitstyp",
"skillName": "Fähigkeitsname", "skillName": "Fähigkeitsname",
"character": "Charakter", "character": "Charakter",
"id": "ID", "id": "ID",
"path": "Pfad", "path": "Pfad",
"rarity": "Seltenheit", "rarity": "Seltenheit",
"element": "Element", "element": "Element",
"technique": "Technik", "technique": "Technik",
"talent": "Talent", "talent": "Talent",
"basic": "Standardangriff", "basic": "Standardangriff",
"skill": "Fähigkeit", "skill": "Fähigkeit",
"ultimate": "Ultimate", "ultimate": "Ultimate",
"servant": "Diener", "servant": "Diener",
"damage": "Schaden", "damage": "Schaden",
"type": "Typ", "type": "Typ",
"warrior": "Die Zerstörung", "warrior": "Die Zerstörung",
"knight": "Die Bewahrung", "knight": "Die Bewahrung",
"mage": "Die Gelehrsamkeit", "mage": "Die Gelehrsamkeit",
"priest": "Der Überfluss", "priest": "Der Überfluss",
"rogue": "Die Jagd", "rogue": "Die Jagd",
"shaman": "Die Harmonie", "shaman": "Die Harmonie",
"warlock": "Die Nichtigkeit", "warlock": "Die Nichtigkeit",
"memory": "Die Erinnerung", "memory": "Die Erinnerung",
"elation": "Die Freude", "elation": "Die Freude",
"fire": "Feuer", "fire": "Feuer",
"ice": "Eis", "ice": "Eis",
"imaginary": "Imaginär", "imaginary": "Imaginär",
"physical": "Physisch", "physical": "Physisch",
"quantum": "Quanten", "quantum": "Quanten",
"thunder": "Blitz", "thunder": "Blitz",
"wind": "Wind", "wind": "Wind",
"hp": "LP", "hp": "LP",
"atk": "ANG", "atk": "ANG",
"speed": "GES", "speed": "GES",
"critRate": "Krit. Rate", "critRate": "Krit. Rate",
"critDmg": "Krit. SCH", "critDmg": "Krit. SCH",
"breakEffect": "Brucheffekt", "breakEffect": "Brucheffekt",
"effectRes": "Effekt-WDS", "effectRes": "Effekt-WDS",
"energyRegenerationRate": "Energie-Regenerationsrate", "energyRegenerationRate": "Energie-Regenerationsrate",
"effectHitRate": "Effekttrefferquote", "effectHitRate": "Effekttrefferquote",
"outgoingHealingBoost": "Ausgehender Heilungsbonus", "outgoingHealingBoost": "Ausgehender Heilungsbonus",
"fireDmgBoost": "Feuer-SCH-Bonus", "fireDmgBoost": "Feuer-SCH-Bonus",
"iceDmgBoost": "Eis-SCH-Bonus", "iceDmgBoost": "Eis-SCH-Bonus",
"imaginaryDmgBoost": "Imaginär-SCH-Bonus", "imaginaryDmgBoost": "Imaginär-SCH-Bonus",
"physicalDmgBoost": "Physisch-SCH-Bonus", "physicalDmgBoost": "Physisch-SCH-Bonus",
"quantumDmgBoost": "Quanten-SCH-Bonus", "quantumDmgBoost": "Quanten-SCH-Bonus",
"thunderDmgBoost": "Blitz-SCH-Bonus", "thunderDmgBoost": "Blitz-SCH-Bonus",
"windDmgBoost": "Wind-SCH-Bonus", "windDmgBoost": "Wind-SCH-Bonus",
"pursued": "Zusätzlicher Schaden", "pursued": "Zusätzlicher Schaden",
"true damage": "Wahrer Schaden", "true damage": "Wahrer Schaden",
"elationdamage": "Freude-Schaden", "elationdamage": "Freude-Schaden",
"follow-up": "Folgeangriff-Schaden", "follow-up": "Folgeangriff-Schaden",
"elemental damage": "Bruch- und Superbruch-Schaden", "elemental damage": "Bruch- und Superbruch-Schaden",
"dot": "Schaden über Zeit", "dot": "Schaden über Zeit",
"qte": "QTE-Fähigkeit", "qte": "QTE-Fähigkeit",
"level": "Stufe", "level": "Stufe",
"relics": "Relikte", "relics": "Relikte",
"eidolons": "Eidolons", "eidolons": "Eidolons",
"lightcones": "Lichtkegel", "lightcones": "Lichtkegel",
"loadData": "Daten laden", "loadData": "Daten laden",
"exportData": "Daten exportieren", "exportData": "Daten exportieren",
"connectSetting": "Verbindungseinstellungen", "connectSetting": "Verbindungseinstellungen",
"connected": "Verbunden", "connected": "Verbunden",
"unconnected": "Nicht verbunden", "unconnected": "Nicht verbunden",
"psConnection": "PS-Verbindung", "psConnection": "PS-Verbindung",
"connectionType": "Verbindungstyp", "connectionType": "Verbindungstyp",
"status": "Status", "status": "Status",
"connectPs": "Mit PS verbinden", "connectPs": "Mit PS verbinden",
"disconnect": "Trennen", "disconnect": "Trennen",
"other": "Andere", "other": "Andere",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Datenbank", "database": "Datenbank",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Monstereinstellungen", "monsterSetting": "Monstereinstellungen",
"serverUrl": "Server-URL", "serverUrl": "Server-URL",
"privateType": "Privater Typ", "privateType": "Privater Typ",
"local": "Lokal", "local": "Lokal",
"server": "Server", "server": "Server",
"username": "Benutzername", "username": "Benutzername",
"password": "Passwort", "password": "Passwort",
"placeholderServerUrl": "Server-URL eingeben", "placeholderServerUrl": "Server-URL eingeben",
"placeholderUsername": "Benutzernamen eingeben", "placeholderUsername": "Benutzernamen eingeben",
"placeholderPassword": "Passwort eingeben", "placeholderPassword": "Passwort eingeben",
"connectedSuccess": "Erfolgreich mit PS verbunden", "connectedSuccess": "Erfolgreich mit PS verbunden",
"connectedFailed": "Verbindung zu PS fehlgeschlagen", "connectedFailed": "Verbindung zu PS fehlgeschlagen",
"syncSuccess": "Daten erfolgreich mit PS synchronisiert", "syncSuccess": "Daten erfolgreich mit PS synchronisiert",
"syncFailed": "Fehler beim Synchronisieren", "syncFailed": "Fehler beim Synchronisieren",
"sync": "Synchronisieren", "sync": "Synchronisieren",
"importSetting": "Import-Einstellungen", "importSetting": "Import-Einstellungen",
"profile": "Profil", "profile": "Profil",
"default": "Standard", "default": "Standard",
"copyProfiles": "Profile kopieren", "copyProfiles": "Profile kopieren",
"addNewProfile": "Neues Profil hinzufügen", "addNewProfile": "Neues Profil hinzufügen",
"createNewProfile": "Neues Profil erstellen", "createNewProfile": "Neues Profil erstellen",
"editProfile": "Profil bearbeiten", "editProfile": "Profil bearbeiten",
"placeholderProfileName": "Profilnamen eingeben", "placeholderProfileName": "Profilnamen eingeben",
"profileName": "Profilname", "profileName": "Profilname",
"create": "Erstellen", "create": "Erstellen",
"update": "Aktualisieren", "update": "Aktualisieren",
"characterInformation": "Charakterinformationen", "characterInformation": "Charakterinformationen",
"skills": "Fähigkeiten", "skills": "Fähigkeiten",
"showcaseCard": "Showcase-Karte", "showcaseCard": "Showcase-Karte",
"comingSoon": "Demnächst", "comingSoon": "Demnächst",
"characterName": "Charaktername", "characterName": "Charaktername",
"placeholderCharacter": "Charakternamen eingeben", "placeholderCharacter": "Charakternamen eingeben",
"characterSettings": "Charaktereinstellungen", "characterSettings": "Charaktereinstellungen",
"levelConfiguration": "Stufenkonfiguration", "levelConfiguration": "Stufenkonfiguration",
"characterLevel": "Charakterstufe", "characterLevel": "Charakterstufe",
"max": "MAX", "max": "MAX",
"ultimateEnergy": "Ultimate-Energie", "ultimateEnergy": "Ultimate-Energie",
"currentEnergy": "Aktuelle Energie", "currentEnergy": "Aktuelle Energie",
"setTo50": "Auf 50% setzen", "setTo50": "Auf 50% setzen",
"battleConfiguration": "Kampfkonfiguration", "battleConfiguration": "Kampfkonfiguration",
"useTechnique": "Technik verwenden", "useTechnique": "Technik verwenden",
"techniqueNote": "Technikeffekte vor dem Kampf aktivieren", "techniqueNote": "Technikeffekte vor dem Kampf aktivieren",
"enhancement": "Verbesserung", "enhancement": "Verbesserung",
"enhancementLevel": "Verbesserungsstufe", "enhancementLevel": "Verbesserungsstufe",
"origin": "Ursprung", "origin": "Ursprung",
"enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei", "enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei",
"lightconeEquipment": "Lichtkegel-Ausrüstung", "lightconeEquipment": "Lichtkegel-Ausrüstung",
"lightconeSettings": "Lichtkegel-Einstellungen", "lightconeSettings": "Lichtkegel-Einstellungen",
"placeholderLevel": "Stufe eingeben", "placeholderLevel": "Stufe eingeben",
"superimpositionRank": "Überlagerungsrang", "superimpositionRank": "Überlagerungsrang",
"ranksNote": "Höhere Ränge bieten stärkere Effekte", "ranksNote": "Höhere Ränge bieten stärkere Effekte",
"changeLightcone": "Lichtkegel wechseln", "changeLightcone": "Lichtkegel wechseln",
"removeLightcone": "Lichtkegel entfernen", "removeLightcone": "Lichtkegel entfernen",
"equipLightcone": "Lichtkegel ausrüsten", "equipLightcone": "Lichtkegel ausrüsten",
"noLightconeEquipped": "Kein Lichtkegel ausgerüstet", "noLightconeEquipped": "Kein Lichtkegel ausgerüstet",
"equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken", "equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken",
"filter": "Filter", "filter": "Filter",
"selectedCharacters": "Ausgewählte Charaktere", "selectedCharacters": "Ausgewählte Charaktere",
"selectedProfiles": "Ausgewählte Profile", "selectedProfiles": "Ausgewählte Profile",
"clearAll": "Alles löschen", "clearAll": "Alles löschen",
"selectAll": "Alles auswählen", "selectAll": "Alles auswählen",
"copy": "Kopieren", "copy": "Kopieren",
"copied": "Kopiert", "copied": "Kopiert",
"noAvatarSelected": "Kein Charakter ausgewählt", "noAvatarSelected": "Kein Charakter ausgewählt",
"noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt", "noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt",
"pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus", "pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus",
"pleaseEnterUid": "Bitte UID eingeben", "pleaseEnterUid": "Bitte UID eingeben",
"failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten", "failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten",
"pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus", "pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus",
"noDataToImport": "Keine Daten zum Importieren", "noDataToImport": "Keine Daten zum Importieren",
"pleaseSelectAFile": "Bitte wähle eine Datei", "pleaseSelectAFile": "Bitte wähle eine Datei",
"fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein", "fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein",
"importEnkaDataSuccess": "Enka-Daten erfolgreich importiert", "importEnkaDataSuccess": "Enka-Daten erfolgreich importiert",
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert", "importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
"importDatabaseSuccess": "Datenbank erfolgreich importiert", "importDatabaseSuccess": "Datenbank erfolgreich importiert",
"getData": "Daten abrufen", "getData": "Daten abrufen",
"import": "Importieren", "import": "Importieren",
"freeSRImport": "FreeSR-Import", "freeSRImport": "FreeSR-Import",
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien", "onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
"pickAFile": "Datei auswählen", "pickAFile": "Datei auswählen",
"lightConeSetting": "Lichtkegel-Einstellung", "lightConeSetting": "Lichtkegel-Einstellung",
"relicMaker": "Relikt-Ersteller", "relicMaker": "Relikt-Ersteller",
"pleaseSelectAllOptions": "Bitte wähle alle Optionen", "pleaseSelectAllOptions": "Bitte wähle alle Optionen",
"relicSavedSuccessfully": "Relikt erfolgreich gespeichert", "relicSavedSuccessfully": "Relikt erfolgreich gespeichert",
"mainSettings": "Haupteinstellungen", "mainSettings": "Haupteinstellungen",
"mainStat": "Hauptwert", "mainStat": "Hauptwert",
"set": "Set", "set": "Set",
"pleaseSelectASet": "Bitte wähle ein Set", "pleaseSelectASet": "Bitte wähle ein Set",
"effectBonus": "Effektbonus", "effectBonus": "Effektbonus",
"totalRoll": "Gesamte Aufwertungen", "totalRoll": "Gesamte Aufwertungen",
"randomizeStats": "Werte zufällig", "randomizeStats": "Werte zufällig",
"randomizeRolls": "Aufwertungen zufällig", "randomizeRolls": "Aufwertungen zufällig",
"selectASubStat": "Wähle einen Nebenwert", "selectASubStat": "Wähle einen Nebenwert",
"selectASet": "Wähle ein Set", "selectASet": "Wähle ein Set",
"selectAMainStat": "Wähle einen Hauptwert", "selectAMainStat": "Wähle einen Hauptwert",
"save": "Speichern", "save": "Speichern",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"roll": "Aufwertung", "roll": "Aufwertung",
"step": "Schritt", "step": "Schritt",
"memoryOfChaos": "Vergessene Halle", "memoryOfChaos": "Vergessene Halle",
"pureFiction": "Reine Fiktion", "pureFiction": "Reine Fiktion",
"apocalypticShadow": "Apokalyptischer Schatten", "apocalypticShadow": "Apokalyptischer Schatten",
"customEnemy": "Benutzerdefinierter Feind", "customEnemy": "Benutzerdefinierter Feind",
"simulatedUniverse": "Universum-Simulation", "simulatedUniverse": "Universum-Simulation",
"floor": "Ebene", "floor": "Ebene",
"side": "Seite", "side": "Seite",
"wave": "Welle", "wave": "Welle",
"stage": "Phase", "stage": "Phase",
"useCycleCount": "Zyklen-Zählung verwenden?", "useCycleCount": "Zyklen-Zählung verwenden?",
"useTurbulenceBuff": "Turbulenz-Buff verwenden?", "useTurbulenceBuff": "Turbulenz-Buff verwenden?",
"firstHalfEnemies": "Gegner erste Hälfte", "firstHalfEnemies": "Gegner erste Hälfte",
"secondHalfEnemies": "Gegner zweite Hälfte", "secondHalfEnemies": "Gegner zweite Hälfte",
"firstNodeEnemies": "Gegner Knoten 1", "firstNodeEnemies": "Gegner Knoten 1",
"secondNodeEnemies": "Gegner Knoten 2", "secondNodeEnemies": "Gegner Knoten 2",
"thirdNodeEnemies": "Gegner Knoten 3", "thirdNodeEnemies": "Gegner Knoten 3",
"firstNode": "Knoten 1", "firstNode": "Knoten 1",
"secondNode": "Knoten 2", "secondNode": "Knoten 2",
"thirdNode": "Knoten 3", "thirdNode": "Knoten 3",
"listEnemies": "Gegnerliste", "listEnemies": "Gegnerliste",
"turbulenceBuff": "Turbulenz-Buff", "turbulenceBuff": "Turbulenz-Buff",
"noEventSelected": "Kein Ereignis ausgewählt", "noEventSelected": "Kein Ereignis ausgewählt",
"noTurbulenceBuff": "Kein Turbulenz-Buff", "noTurbulenceBuff": "Kein Turbulenz-Buff",
"upper": "Oben", "upper": "Oben",
"lower": "Unten", "lower": "Unten",
"upperToLower": "Oben -> Unten", "upperToLower": "Oben -> Unten",
"lowerToUpper": "Unten -> Oben", "lowerToUpper": "Unten -> Oben",
"selectMOCEvent": "MOC-Ereignis wählen", "selectMOCEvent": "MOC-Ereignis wählen",
"selectPFEvent": "PF-Ereignis wählen", "selectPFEvent": "PF-Ereignis wählen",
"selectASEvent": "AS-Ereignis wählen", "selectASEvent": "AS-Ereignis wählen",
"selectCEEvent": "CE-Ereignis wählen", "selectCEEvent": "CE-Ereignis wählen",
"selectEvent": "Ereignis wählen", "selectEvent": "Ereignis wählen",
"selectFloor": "Ebene wählen", "selectFloor": "Ebene wählen",
"selectSide": "Seite wählen", "selectSide": "Seite wählen",
"selectBuff": "Buff wählen", "selectBuff": "Buff wählen",
"selectStage": "Phase wählen", "selectStage": "Phase wählen",
"previous": "Zurück", "previous": "Zurück",
"next": "Weiter", "next": "Weiter",
"noMonstersFound": "Keine Monster gefunden", "noMonstersFound": "Keine Monster gefunden",
"addNewWave": "Neue Welle hinzufügen", "addNewWave": "Neue Welle hinzufügen",
"searchStage": "Phase suchen...", "searchStage": "Phase suchen...",
"noStageFound": "Keine Phase gefunden", "noStageFound": "Keine Phase gefunden",
"searchMonster": "Monster suchen...", "searchMonster": "Monster suchen...",
"changeRelic": "Relikt wechseln", "changeRelic": "Relikt wechseln",
"deleteRelic": "Relikt löschen", "deleteRelic": "Relikt löschen",
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?", "deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
"setEffects": "Effekte einstellen", "setEffects": "Effekte einstellen",
"details": "Details", "details": "Details",
"normal": "Standardangriff", "normal": "Standardangriff",
"bpskill": "Fähigkeit", "bpskill": "Fähigkeit",
"maze": "Technik", "maze": "Technik",
"ultra": "Ultimate", "ultra": "Ultimate",
"servantskill": "Memosprite-Fähigkeit", "assist": "Assist-Fähigkeit",
"severaltalent": "Memosprite-Talent", "servantskill": "Memosprite-Fähigkeit",
"singleattack": "Einzelangriff", "severaltalent": "Memosprite-Talent",
"enhance": "Verbessern", "singleattack": "Einzelangriff",
"summon": "Beschwören", "enhance": "Verbessern",
"mazeattack": "Technik-Angriff", "summon": "Beschwören",
"blast": "Explosion", "mazeattack": "Technik-Angriff",
"restore": "Wiederherstellen", "blast": "Explosion",
"support": "Unterstützung", "restore": "Wiederherstellen",
"aoeattack": "Flächenangriff", "support": "Unterstützung",
"impair": "Beeinträchtigen", "aoeattack": "Flächenangriff",
"bounce": "Abprallen", "impair": "Beeinträchtigen",
"active": "Aktiv", "bounce": "Abprallen",
"defence": "Verteidigung", "active": "Aktiv",
"inactive": "Inaktiv", "defence": "Verteidigung",
"maxAll": "Alles maximieren", "inactive": "Inaktiv",
"maxAllSuccess": "Fähigkeiten erfolgreich maximiert.", "maxAll": "Alles maximieren",
"maxAllFailed": "Fehler beim Maximieren.", "maxAllSuccess": "Fähigkeiten erfolgreich maximiert.",
"noRelicEquipped": "Kein Relikt ausgerüstet", "maxAllFailed": "Fehler beim Maximieren.",
"anomalyArbitration": "Anomalie-Schiedsgericht", "noRelicEquipped": "Kein Relikt ausgerüstet",
"normalMode": "Normaler Modus", "anomalyArbitration": "Anomalie-Schiedsgericht",
"hardMode": "Schwerer Modus", "normalMode": "Normaler Modus",
"selectPEAKEvent": "PEAK-Ereignis wählen", "hardMode": "Schwerer Modus",
"mode": "Modus", "selectPEAKEvent": "PEAK-Ereignis wählen",
"selectMode": "Modus wählen", "mode": "Modus",
"rollBack": "Rückgängig", "selectMode": "Modus wählen",
"upRoll": "Aufwertung hoch", "rollBack": "Rückgängig",
"downRoll": "Aufwertung runter", "upRoll": "Aufwertung hoch",
"actions": "Aktionen", "downRoll": "Aufwertung runter",
"avatars": "Avatare", "actions": "Aktionen",
"quickView": "Schnellansicht", "avatars": "Avatare",
"extraSetting": "Extra-Einstellungen", "quickView": "Schnellansicht",
"disableCensorship": "Zensur deaktivieren", "extraSetting": "Extra-Einstellungen",
"hideUI": "UI verstecken", "disableCensorship": "Zensur deaktivieren",
"theoryCraftMode": "Theorycraft-Modus", "hideUI": "UI verstecken",
"cycleCount": "Zyklen-Anzahl", "theoryCraftMode": "Theorycraft-Modus",
"pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen", "cycleCount": "Zyklen-Anzahl",
"subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein", "pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein",
"multipathCharacter": "Mehrpfad-Charakter", "theoryCraft": "Theorycraft",
"mainPath": "Hauptpfad", "multipathCharacter": "Mehrpfad-Charakter",
"march7Path": "Pfad 7. März", "mainPath": "Hauptpfad",
"challenge": "Herausforderung", "march7Path": "Pfad 7. März",
"skipNode": "Knoten überspringen", "challenge": "Herausforderung",
"disableSkip": "Überspringen deaktivieren", "skipNode": "Knoten überspringen",
"skipNode1": "Knoten 1 überspringen", "disableSkip": "Überspringen deaktivieren",
"skipNode2": "Knoten 2 überspringen", "skipNode1": "Knoten 1 überspringen",
"extraFeatures": "Zusatzfunktionen", "skipNode2": "Knoten 2 überspringen",
"detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.", "extraFeatures": "Zusatzfunktionen",
"detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.", "detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.",
"detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.", "detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.",
"detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.", "detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.",
"detailDisableCensorship": "Deaktiviert die Zensur im Spiel.", "detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.",
"detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.", "detailDisableCensorship": "Deaktiviert die Zensur im Spiel.",
"trailblazer": "Trailblazer", "detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.",
"listExtraEffect": "Liste Zusatzeffekte", "trailblazer": "Trailblazer",
"extra": "Extra", "listExtraEffect": "Liste Zusatzeffekte",
"customLineup": "Benutzerdefinierte Aufstellung" "extra": "Extra",
} "customLineup": "Benutzerdefinierte Aufstellung",
} "def": "VTD",
"language": "Sprache"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Skill Type", "skillType": "Skill Type",
"skillName": "Skill Name", "skillName": "Skill Name",
"character": "Character", "character": "Character",
"id": "Id", "id": "Id",
"path": "Path", "path": "Path",
"rarity": "Rarity", "rarity": "Rarity",
"element": "Element", "element": "Element",
"technique": "Technique", "technique": "Technique",
"talent": "Talent", "talent": "Talent",
"basic": "Basic Attack", "basic": "Basic Attack",
"skill": "Skill", "skill": "Skill",
"ultimate": "Ultimate", "ultimate": "Ultimate",
"servant": "Servant", "servant": "Servant",
"damage": "Damage", "damage": "Damage",
"type": "Type", "type": "Type",
"warrior": "The Destruction", "warrior": "The Destruction",
"knight": "The Preservation", "knight": "The Preservation",
"mage": "The Erudition", "mage": "The Erudition",
"priest": "The Abundance", "priest": "The Abundance",
"rogue": "The Hunt", "rogue": "The Hunt",
"shaman": "The Harmony", "shaman": "The Harmony",
"warlock": "The Nihility", "warlock": "The Nihility",
"memory": "The Remembrance", "memory": "The Remembrance",
"elation": "The Elation", "elation": "The Elation",
"fire": "Fire", "fire": "Fire",
"ice": "Ice", "ice": "Ice",
"imaginary": "Imaginary", "imaginary": "Imaginary",
"physical": "Physical", "physical": "Physical",
"quantum": "Quantum", "quantum": "Quantum",
"thunder": "Thunder", "thunder": "Thunder",
"wind": "Wind", "wind": "Wind",
"hp": "Hp", "hp": "Hp",
"atk": "Atk", "atk": "Atk",
"speed": "Speed", "def": "Def",
"critRate": "Crit Rate", "speed": "Speed",
"critDmg": "Crit Dmg", "critRate": "Crit Rate",
"breakEffect": "Break Effect", "critDmg": "Crit Dmg",
"effectRes": "Effect Res", "breakEffect": "Break Effect",
"energyRegenerationRate": "Energy Regeneration Rate", "effectRes": "Effect Res",
"effectHitRate": "Effect Hit Rate", "energyRegenerationRate": "Energy Regeneration Rate",
"outgoingHealingBoost": "Outgoing Healing Boost", "effectHitRate": "Effect Hit Rate",
"fireDmgBoost": "Fire damage boost", "outgoingHealingBoost": "Outgoing Healing Boost",
"iceDmgBoost": "Ice damage Boost", "fireDmgBoost": "Fire damage boost",
"imaginaryDmgBoost": "Imaginary damage boost", "iceDmgBoost": "Ice damage Boost",
"physicalDmgBoost": "Physical damage boost", "imaginaryDmgBoost": "Imaginary damage boost",
"quantumDmgBoost": "Quantum damage boost", "physicalDmgBoost": "Physical damage boost",
"thunderDmgBoost": "Thunder damage boost", "quantumDmgBoost": "Quantum damage boost",
"windDmgBoost": "Wind damage boost", "thunderDmgBoost": "Thunder damage boost",
"pursued": "Additional damage", "windDmgBoost": "Wind damage boost",
"true damage": "True damage", "pursued": "Additional damage",
"elationdamage": "Elation damage", "true damage": "True damage",
"follow-up": "Follow-up Damage", "elationdamage": "Elation damage",
"elemental damage": "Break and Super break damage", "follow-up": "Follow-up Damage",
"dot": "Damage over time ", "elemental damage": "Break and Super break damage",
"qte": "QTE Skill", "dot": "Damage over time ",
"level": "Level", "qte": "QTE Skill",
"relics": "Relics", "level": "Level",
"eidolons": "Eidolons", "relics": "Relics",
"lightcones": "Lightcones", "eidolons": "Eidolons",
"loadData": "Load data", "lightcones": "Lightcones",
"exportData": "Export data", "loadData": "Load data",
"connectSetting": "Connection Setting", "exportData": "Export data",
"connected": "Connected", "connectSetting": "Connection Setting",
"unconnected": "Unconnected", "connected": "Connected",
"psConnection": "PS Connection", "unconnected": "Unconnected",
"connectionType": "Connection Type", "psConnection": "PS Connection",
"status": "Status", "connectionType": "Connection Type",
"connectPs": "Connect PS", "status": "Status",
"disconnect": "Disconnect", "connectPs": "Connect PS",
"other": "Other", "disconnect": "Disconnect",
"freeSr": "FreeSR", "other": "Other",
"database": "Database", "freeSr": "FreeSR",
"enka": "Enka", "database": "Database",
"monsterSetting": "Monster Setting", "enka": "Enka",
"serverUrl": "Server URL", "monsterSetting": "Monster Setting",
"privateType": "Private Type", "serverUrl": "Server URL",
"local": "Local", "privateType": "Private Type",
"server": "Server", "local": "Local",
"username": "Username", "server": "Server",
"password": "Password", "username": "Username",
"placeholderServerUrl": "Enter server URL", "password": "Password",
"placeholderUsername": "Enter username", "placeholderServerUrl": "Enter server URL",
"placeholderPassword": "Enter password", "placeholderUsername": "Enter username",
"connectedSuccess": "Connected to PS successfully", "placeholderPassword": "Enter password",
"connectedFailed": "Failed to connect to PS", "connectedSuccess": "Connected to PS successfully",
"syncSuccess": "Synced data to PS successfully", "connectedFailed": "Failed to connect to PS",
"syncFailed": "Failed to sync data to PS", "syncSuccess": "Synced data to PS successfully",
"sync": "Sync", "syncFailed": "Failed to sync data to PS",
"importSetting": "Import Setting", "sync": "Sync",
"profile": "Profile", "importSetting": "Import Setting",
"default": "Default", "profile": "Profile",
"copyProfiles": "Copy profiles", "default": "Default",
"addNewProfile": "Add new profile", "copyProfiles": "Copy profiles",
"createNewProfile": "Create new profile", "addNewProfile": "Add new profile",
"editProfile": "Edit profile", "createNewProfile": "Create new profile",
"placeholderProfileName": "Enter profile name", "editProfile": "Edit profile",
"profileName": "Profile name", "placeholderProfileName": "Enter profile name",
"create": "Create", "profileName": "Profile name",
"update": "Update", "create": "Create",
"characterInformation": "Character Information", "update": "Update",
"skills": "Skills", "characterInformation": "Character Information",
"showcaseCard": "Showcase Card", "skills": "Skills",
"comingSoon": "Coming soon", "showcaseCard": "Showcase Card",
"characterName": "Character name", "comingSoon": "Coming soon",
"placeholderCharacter": "Enter character name", "characterName": "Character name",
"characterSettings": "Character Settings", "placeholderCharacter": "Enter character name",
"levelConfiguration": "Level Configuration", "characterSettings": "Character Settings",
"characterLevel": "Character Level", "levelConfiguration": "Level Configuration",
"max": "MAX", "characterLevel": "Character Level",
"ultimateEnergy": "Ultimate Energy", "max": "MAX",
"currentEnergy": "Current Energy", "ultimateEnergy": "Ultimate Energy",
"setTo50": "Set to 50%", "currentEnergy": "Current Energy",
"battleConfiguration": "Battle Configuration", "setTo50": "Set to 50%",
"useTechnique": "Use Technique", "battleConfiguration": "Battle Configuration",
"techniqueNote": "Enable pre-battle technique effects", "useTechnique": "Use Technique",
"enhancement": "Enhancement", "techniqueNote": "Enable pre-battle technique effects",
"enhancementLevel": "Enhancement Level", "enhancement": "Enhancement",
"origin": "Origin", "enhancementLevel": "Enhancement Level",
"enhancedNote": "Higher enhanced unlock additional abilities", "origin": "Origin",
"lightconeEquipment": "Lightcone Equipment", "enhancedNote": "Higher enhanced unlock additional abilities",
"lightconeSettings": "Lightcone Settings", "lightconeEquipment": "Lightcone Equipment",
"placeholderLevel": "Enter level", "lightconeSettings": "Lightcone Settings",
"superimpositionRank": "Superimposition Rank", "placeholderLevel": "Enter level",
"ranksNote": "Higher ranks provide stronger effects", "superimpositionRank": "Superimposition Rank",
"changeLightcone": "Change Lightcone", "ranksNote": "Higher ranks provide stronger effects",
"removeLightcone": "Remove Lightcone", "changeLightcone": "Change Lightcone",
"equipLightcone": "Equip Lightcone", "removeLightcone": "Remove Lightcone",
"noLightconeEquipped": "No Lightcone Equipped", "equipLightcone": "Equip Lightcone",
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities", "noLightconeEquipped": "No Lightcone Equipped",
"filter": "Filter", "equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
"selectedCharacters": "Selected Characters", "filter": "Filter",
"selectedProfiles": "Selected Profiles", "selectedCharacters": "Selected Characters",
"clearAll": "Clear All", "selectedProfiles": "Selected Profiles",
"selectAll": "Select All", "clearAll": "Clear All",
"copy": "Copy", "selectAll": "Select All",
"copied": "Copied", "copy": "Copy",
"noAvatarSelected": "No avatar selected", "copied": "Copied",
"noAvatarToCopySelected": "No avatar to copy selected", "noAvatarSelected": "No avatar selected",
"pleaseSelectAtLeastOneProfile": "Please select at least one profile", "noAvatarToCopySelected": "No avatar to copy selected",
"pleaseEnterUid": "Please enter UID", "pleaseSelectAtLeastOneProfile": "Please select at least one profile",
"failedToFetchEnkaData": "Failed to fetch enka data", "pleaseEnterUid": "Please enter UID",
"pleaseSelectAtLeastOneCharacter": "Please select at least one character", "failedToFetchEnkaData": "Failed to fetch enka data",
"noDataToImport": "No data to import", "pleaseSelectAtLeastOneCharacter": "Please select at least one character",
"pleaseSelectAFile": "Please select a file", "noDataToImport": "No data to import",
"fileMustBeAValidJsonFile": "File must be a valid json file", "pleaseSelectAFile": "Please select a file",
"importEnkaDataSuccess": "Import Enka data success", "fileMustBeAValidJsonFile": "File must be a valid json file",
"importFreeSRDataSuccess": "Import FreeSR data success", "importEnkaDataSuccess": "Import Enka data success",
"importDatabaseSuccess": "Import database success", "importFreeSRDataSuccess": "Import FreeSR data success",
"getData": "Get Data", "importDatabaseSuccess": "Import database success",
"import": "Import", "getData": "Get Data",
"freeSRImport": "FreeSR Import", "import": "Import",
"onlySupportFreeSRJsonFile": "Only support FreeSR json file", "freeSRImport": "FreeSR Import",
"pickAFile": "Pick a file", "onlySupportFreeSRJsonFile": "Only support FreeSR json file",
"lightConeSetting": "LightCone Setting", "pickAFile": "Pick a file",
"relicMaker": "Relic Maker", "lightConeSetting": "LightCone Setting",
"pleaseSelectAllOptions": "Please select all options", "relicMaker": "Relic Maker",
"relicSavedSuccessfully": "Relic saved successfully", "pleaseSelectAllOptions": "Please select all options",
"mainSettings": "Main Settings", "relicSavedSuccessfully": "Relic saved successfully",
"mainStat": "Main Stat", "mainSettings": "Main Settings",
"set": "Set", "mainStat": "Main Stat",
"pleaseSelectASet": "Please select a set", "set": "Set",
"effectBonus": "Effect Bonus", "pleaseSelectASet": "Please select a set",
"totalRoll": "Total Roll", "effectBonus": "Effect Bonus",
"randomizeStats": "Randomize Stats", "totalRoll": "Total Roll",
"randomizeRolls": "Randomize Rolls", "randomizeStats": "Randomize Stats",
"selectASubStat": "Select a sub stat", "randomizeRolls": "Randomize Rolls",
"selectASet": "Select a set", "selectASubStat": "Select a sub stat",
"selectAMainStat": "Select a main stat", "selectASet": "Select a set",
"save": "Save", "selectAMainStat": "Select a main stat",
"reset": "Reset", "save": "Save",
"roll": "Roll", "reset": "Reset",
"step": "Step", "roll": "Roll",
"memoryOfChaos": "Memory of Chaos", "step": "Step",
"pureFiction": "Pure Fiction", "memoryOfChaos": "Memory of Chaos",
"apocalypticShadow": "Apocalyptic Shadow", "pureFiction": "Pure Fiction",
"customEnemy": "Custom Enemy", "apocalypticShadow": "Apocalyptic Shadow",
"simulatedUniverse": "Simulated Universe", "customEnemy": "Custom Enemy",
"floor": "Floor", "simulatedUniverse": "Simulated Universe",
"side": "Side", "floor": "Floor",
"wave": "Wave", "side": "Side",
"stage": "Stage", "wave": "Wave",
"useCycleCount": "Use cycle count?", "stage": "Stage",
"useTurbulenceBuff": "Use turbulence buff?", "useCycleCount": "Use cycle count?",
"firstHalfEnemies": "First half enemies", "useTurbulenceBuff": "Use turbulence buff?",
"secondHalfEnemies": "Second half enemies", "firstHalfEnemies": "First half enemies",
"firstNodeEnemies": "First node enemies", "secondHalfEnemies": "Second half enemies",
"secondNodeEnemies": "Second node enemies", "firstNodeEnemies": "First node enemies",
"thirdNodeEnemies": "Third node enemies", "secondNodeEnemies": "Second node enemies",
"firstNode": "First Node", "thirdNodeEnemies": "Third node enemies",
"secondNode": "Second Node", "firstNode": "First Node",
"thirdNode": "Third Node", "secondNode": "Second Node",
"listEnemies": "List enemies", "thirdNode": "Third Node",
"turbulenceBuff": "Turbulence Buff", "listEnemies": "List enemies",
"noEventSelected": "No event selected", "turbulenceBuff": "Turbulence Buff",
"noTurbulenceBuff": "No Turbulence Buff", "noEventSelected": "No event selected",
"upper": "Upper", "noTurbulenceBuff": "No Turbulence Buff",
"lower": "Lower", "upper": "Upper",
"upperToLower": "Upper -> Lower", "lower": "Lower",
"lowerToUpper": "Lower -> Upper", "upperToLower": "Upper -> Lower",
"selectMOCEvent": "Select MOC Event", "lowerToUpper": "Lower -> Upper",
"selectPFEvent": "Select PF Event", "selectMOCEvent": "Select MOC Event",
"selectASEvent": "Select AS Event", "selectPFEvent": "Select PF Event",
"selectCEEvent": "Select CE Event", "selectASEvent": "Select AS Event",
"selectEvent": "Select Event", "selectCEEvent": "Select CE Event",
"selectFloor": "Select a Floor", "selectEvent": "Select Event",
"selectSide": "Select a Side", "selectFloor": "Select a Floor",
"selectBuff": "Select a Buff", "selectSide": "Select a Side",
"selectStage": "Select a Stage", "selectBuff": "Select a Buff",
"previous": "Previous", "selectStage": "Select a Stage",
"next": "Next", "previous": "Previous",
"noMonstersFound": "No monsters found", "next": "Next",
"addNewWave": "Add New Wave", "noMonstersFound": "No monsters found",
"searchStage": "Search stage...", "addNewWave": "Add New Wave",
"noStageFound": "No stage found", "searchStage": "Search stage...",
"searchMonster": "Search monster...", "noStageFound": "No stage found",
"changeRelic": "Change relic", "searchMonster": "Search monster...",
"deleteRelic": "Delete relic", "changeRelic": "Change relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot", "deleteRelic": "Delete relic",
"setEffects": "Set Effects", "deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"details": "Details", "setEffects": "Set Effects",
"normal": "Basic ATK", "details": "Details",
"bpskill": "Skill", "normal": "Basic ATK",
"maze": "Technique", "bpskill": "Skill",
"ultra": "Ultimate", "maze": "Technique",
"servantskill": "Memosprite Skill", "ultra": "Ultimate",
"severaltalent": "Memosprite Talent", "assist": "Assist Skill",
"singleattack": "Single Attack", "servantskill": "Memosprite Skill",
"enhance": "Enhance", "severaltalent": "Memosprite Talent",
"summon": "Summon", "singleattack": "Single Attack",
"mazeattack": "Technique Attack", "enhance": "Enhance",
"blast": "Blast", "summon": "Summon",
"restore": "Restore", "mazeattack": "Technique Attack",
"support": "Support", "blast": "Blast",
"aoeattack": "AoE Attack", "restore": "Restore",
"impair": "Impair", "support": "Support",
"bounce": "Bounce", "aoeattack": "AoE Attack",
"active": "Active", "impair": "Impair",
"defence": "Defence", "bounce": "Bounce",
"inactive": "Inactive", "active": "Active",
"maxAll": "Max All", "defence": "Defence",
"maxAllSuccess": "Successfully set skill level to max.", "inactive": "Inactive",
"maxAllFailed": "Failed to set skill level to max.", "maxAll": "Max All",
"noRelicEquipped": "No relic equipped", "maxAllSuccess": "Successfully set skill level to max.",
"anomalyArbitration": "Anomaly Arbitration", "maxAllFailed": "Failed to set skill level to max.",
"normalMode": "Normal Mode", "noRelicEquipped": "No relic equipped",
"hardMode": "Hard Mode", "anomalyArbitration": "Anomaly Arbitration",
"selectPEAKEvent": "Select PEAK Event", "normalMode": "Normal Mode",
"mode": "Mode", "hardMode": "Hard Mode",
"selectMode": "Select a mode", "selectPEAKEvent": "Select PEAK Event",
"rollBack": "Roll Back", "mode": "Mode",
"upRoll": "Up Roll", "selectMode": "Select a mode",
"downRoll": "Down Roll", "rollBack": "Roll Back",
"actions": "Actions", "upRoll": "Up Roll",
"avatars": "Avatars", "downRoll": "Down Roll",
"quickView": "Quick View", "actions": "Actions",
"extraSetting": "Extra Settings", "avatars": "Avatars",
"disableCensorship": "Disable Censorship", "quickView": "Quick View",
"hideUI": "Hide UI", "extraSetting": "Extra Settings",
"theoryCraftMode": "Theorycraft Mode", "disableCensorship": "Disable Censorship",
"cycleCount": "Cycle Count", "hideUI": "Hide UI",
"pleaseSelectAllSubStats": "Please select all sub stats", "theoryCraftMode": "Theorycraft Mode",
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero", "cycleCount": "Cycle Count",
"theoryCraft": "Theorycraft", "pleaseSelectAllSubStats": "Please select all sub stats",
"multipathCharacter": "Multipath Character", "subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
"mainPath": "Main Path", "theoryCraft": "Theorycraft",
"march7Path": "March 7 Path", "multipathCharacter": "Multipath Character",
"challenge": "Challenge", "mainPath": "Main Path",
"skipNode": "Skip Node", "march7Path": "March 7 Path",
"disableSkip": "Disable skip", "challenge": "Challenge",
"skipNode1": "Skip node 1", "skipNode": "Skip Node",
"skipNode2": "Skip node 2", "disableSkip": "Disable skip",
"extraFeatures": "Extra Features", "skipNode1": "Skip node 1",
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.", "skipNode2": "Skip node 2",
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.", "extraFeatures": "Extra Features",
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.", "detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
"detailHiddenUi": "Enabling this feature will hide the game UI.", "detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.", "detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
"detailMultipathCharacter": "Allows changing the Path of certain characters.", "detailHiddenUi": "Enabling this feature will hide the game UI.",
"trailblazer": "Trailblazer", "detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"listExtraEffect": "List Extra Effect", "detailMultipathCharacter": "Allows changing the Path of certain characters.",
"extra": "Extra", "trailblazer": "Trailblazer",
"customLineup": "Custom Lineup" "listExtraEffect": "List Extra Effect",
} "extra": "Extra",
} "customLineup": "Custom Lineup",
"language": "Language"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Herramientas Firefly por Firefly Shelter" "description": "Herramientas Firefly por Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Tipo de habilidad", "skillType": "Tipo de habilidad",
"skillName": "Nombre de la habilidad", "skillName": "Nombre de la habilidad",
"character": "Personaje", "character": "Personaje",
"id": "Id", "id": "Id",
"path": "Vía", "path": "Vía",
"rarity": "Rareza", "rarity": "Rareza",
"element": "Elemento", "element": "Elemento",
"technique": "Técnica", "technique": "Técnica",
"talent": "Talento", "talent": "Talento",
"basic": "Ataque Básico", "basic": "Ataque Básico",
"skill": "Habilidad", "skill": "Habilidad",
"ultimate": "Habilidad Definitiva", "ultimate": "Habilidad Definitiva",
"servant": "Mnemoduende", "servant": "Mnemoduende",
"damage": "Daño", "damage": "Daño",
"type": "Tipo", "type": "Tipo",
"warrior": "La Destrucción", "warrior": "La Destrucción",
"knight": "La Conservación", "knight": "La Conservación",
"mage": "La Erudición", "mage": "La Erudición",
"priest": "La Abundancia", "priest": "La Abundancia",
"rogue": "La Cacería", "rogue": "La Cacería",
"shaman": "La Armonía", "shaman": "La Armonía",
"warlock": "La Nihilidad", "warlock": "La Nihilidad",
"memory": "La Reminiscencia", "memory": "La Reminiscencia",
"elation": "La Exultación", "elation": "La Exultación",
"fire": "Fuego", "fire": "Fuego",
"ice": "Hielo", "ice": "Hielo",
"imaginary": "Imaginario", "imaginary": "Imaginario",
"physical": "Físico", "physical": "Físico",
"quantum": "Cuántico", "quantum": "Cuántico",
"thunder": "Rayo", "thunder": "Rayo",
"wind": "Viento", "wind": "Viento",
"hp": "PV", "hp": "PV",
"atk": "ATQ", "atk": "ATQ",
"speed": "VEL", "speed": "VEL",
"critRate": "Prob. CRIT", "critRate": "Prob. CRIT",
"critDmg": "Daño CRIT", "critDmg": "Daño CRIT",
"breakEffect": "Efecto de Ruptura", "breakEffect": "Efecto de Ruptura",
"effectRes": "RES a Efecto", "effectRes": "RES a Efecto",
"energyRegenerationRate": "Recuperación de Energía", "energyRegenerationRate": "Recuperación de Energía",
"effectHitRate": "Acierto de Efecto", "effectHitRate": "Acierto de Efecto",
"outgoingHealingBoost": "Bono de Curación", "outgoingHealingBoost": "Bono de Curación",
"fireDmgBoost": "Aumento de Daño de Fuego", "fireDmgBoost": "Aumento de Daño de Fuego",
"iceDmgBoost": "Aumento de Daño de Hielo", "iceDmgBoost": "Aumento de Daño de Hielo",
"imaginaryDmgBoost": "Aumento de Daño Imaginario", "imaginaryDmgBoost": "Aumento de Daño Imaginario",
"physicalDmgBoost": "Aumento de Daño Físico", "physicalDmgBoost": "Aumento de Daño Físico",
"quantumDmgBoost": "Aumento de Daño Cuántico", "quantumDmgBoost": "Aumento de Daño Cuántico",
"thunderDmgBoost": "Aumento de Daño de Rayo", "thunderDmgBoost": "Aumento de Daño de Rayo",
"windDmgBoost": "Aumento de Daño de Viento", "windDmgBoost": "Aumento de Daño de Viento",
"pursued": "Daño adicional", "pursued": "Daño adicional",
"true damage": "Daño verdadero", "true damage": "Daño verdadero",
"elationdamage": "Daño de Exultación", "elationdamage": "Daño de Exultación",
"follow-up": "Daño de Ataque Adicional", "follow-up": "Daño de Ataque Adicional",
"elemental damage": "Daño de Ruptura y Superruptura", "elemental damage": "Daño de Ruptura y Superruptura",
"dot": "Daño con el tiempo", "dot": "Daño con el tiempo",
"qte": "Habilidad QTE", "qte": "Habilidad QTE",
"level": "Nivel", "level": "Nivel",
"relics": "Artefactos", "relics": "Artefactos",
"eidolons": "Eidolones", "eidolons": "Eidolones",
"lightcones": "Conos de Luz", "lightcones": "Conos de Luz",
"loadData": "Cargar datos", "loadData": "Cargar datos",
"exportData": "Exportar datos", "exportData": "Exportar datos",
"connectSetting": "Ajustes de conexión", "connectSetting": "Ajustes de conexión",
"connected": "Conectado", "connected": "Conectado",
"unconnected": "Desconectado", "unconnected": "Desconectado",
"psConnection": "Conexión PS", "psConnection": "Conexión PS",
"connectionType": "Tipo de conexión", "connectionType": "Tipo de conexión",
"status": "Estado", "status": "Estado",
"connectPs": "Conectar PS", "connectPs": "Conectar PS",
"disconnect": "Desconectar", "disconnect": "Desconectar",
"other": "Otro", "other": "Otro",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Base de datos", "database": "Base de datos",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Ajuste de Monstruos", "monsterSetting": "Ajuste de Monstruos",
"serverUrl": "URL del Servidor", "serverUrl": "URL del Servidor",
"privateType": "Tipo privado", "privateType": "Tipo privado",
"local": "Local", "local": "Local",
"server": "Servidor", "server": "Servidor",
"username": "Usuario", "username": "Usuario",
"password": "Contraseña", "password": "Contraseña",
"placeholderServerUrl": "Introducir URL del servidor", "placeholderServerUrl": "Introducir URL del servidor",
"placeholderUsername": "Introducir usuario", "placeholderUsername": "Introducir usuario",
"placeholderPassword": "Introducir contraseña", "placeholderPassword": "Introducir contraseña",
"connectedSuccess": "Conectado a PS exitosamente", "connectedSuccess": "Conectado a PS exitosamente",
"connectedFailed": "Fallo al conectar a PS", "connectedFailed": "Fallo al conectar a PS",
"syncSuccess": "Datos sincronizados a PS exitosamente", "syncSuccess": "Datos sincronizados a PS exitosamente",
"syncFailed": "Fallo al sincronizar datos a PS", "syncFailed": "Fallo al sincronizar datos a PS",
"sync": "Sincronizar", "sync": "Sincronizar",
"importSetting": "Ajustes de Importación", "importSetting": "Ajustes de Importación",
"profile": "Perfil", "profile": "Perfil",
"default": "Por defecto", "default": "Por defecto",
"copyProfiles": "Copiar perfiles", "copyProfiles": "Copiar perfiles",
"addNewProfile": "Añadir nuevo perfil", "addNewProfile": "Añadir nuevo perfil",
"createNewProfile": "Crear nuevo perfil", "createNewProfile": "Crear nuevo perfil",
"editProfile": "Editar perfil", "editProfile": "Editar perfil",
"placeholderProfileName": "Introducir nombre de perfil", "placeholderProfileName": "Introducir nombre de perfil",
"profileName": "Nombre del perfil", "profileName": "Nombre del perfil",
"create": "Crear", "create": "Crear",
"update": "Actualizar", "update": "Actualizar",
"characterInformation": "Información del personaje", "characterInformation": "Información del personaje",
"skills": "Habilidades", "skills": "Habilidades",
"showcaseCard": "Tarjeta de presentación del personaje", "showcaseCard": "Tarjeta de presentación del personaje",
"comingSoon": "Próximamente", "comingSoon": "Próximamente",
"characterName": "Nombre del personaje", "characterName": "Nombre del personaje",
"placeholderCharacter": "Introducir nombre del personaje", "placeholderCharacter": "Introducir nombre del personaje",
"characterSettings": "Ajustes del Personaje", "characterSettings": "Ajustes del Personaje",
"levelConfiguration": "Configuración de Nivel", "levelConfiguration": "Configuración de Nivel",
"characterLevel": "Nivel de Personaje", "characterLevel": "Nivel de Personaje",
"max": "MÁX", "max": "MÁX",
"ultimateEnergy": "Energía de Habilidad Definitiva", "ultimateEnergy": "Energía de Habilidad Definitiva",
"currentEnergy": "Energía al inicio del combate", "currentEnergy": "Energía al inicio del combate",
"setTo50": "Ajustar al 50%", "setTo50": "Ajustar al 50%",
"battleConfiguration": "Configuración de Batalla", "battleConfiguration": "Configuración de Batalla",
"useTechnique": "Usar Técnica", "useTechnique": "Usar Técnica",
"techniqueNote": "Activar efectos de técnica pre-batalla", "techniqueNote": "Activar efectos de técnica pre-batalla",
"enhancement": "Mejora", "enhancement": "Mejora",
"enhancementLevel": "Nivel de Mejora", "enhancementLevel": "Nivel de Mejora",
"origin": "Original", "origin": "Original",
"enhancedNote": "Niveles superiores desbloquean más habilidades", "enhancedNote": "Niveles superiores desbloquean más habilidades",
"lightconeEquipment": "Equipamiento de Cono de Luz", "lightconeEquipment": "Equipamiento de Cono de Luz",
"lightconeSettings": "Ajustes de Cono de Luz", "lightconeSettings": "Ajustes de Cono de Luz",
"placeholderLevel": "Introducir nivel", "placeholderLevel": "Introducir nivel",
"superimpositionRank": "Rango de Superposición", "superimpositionRank": "Rango de Superposición",
"ranksNote": "Rangos mayores ofrecen efectos más fuertes", "ranksNote": "Rangos mayores ofrecen efectos más fuertes",
"changeLightcone": "Cambiar Cono de Luz", "changeLightcone": "Cambiar Cono de Luz",
"removeLightcone": "Quitar Cono de Luz", "removeLightcone": "Quitar Cono de Luz",
"equipLightcone": "Equipar Cono de Luz", "equipLightcone": "Equipar Cono de Luz",
"noLightconeEquipped": "Sin Cono de Luz", "noLightconeEquipped": "Sin Cono de Luz",
"equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje", "equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje",
"filter": "Filtro", "filter": "Filtro",
"selectedCharacters": "Personajes Seleccionados", "selectedCharacters": "Personajes Seleccionados",
"selectedProfiles": "Perfiles Seleccionados", "selectedProfiles": "Perfiles Seleccionados",
"clearAll": "Limpiar Todo", "clearAll": "Limpiar Todo",
"selectAll": "Seleccionar Todo", "selectAll": "Seleccionar Todo",
"copy": "Copiar", "copy": "Copiar",
"copied": "Copiado", "copied": "Copiado",
"noAvatarSelected": "Ningún personaje seleccionado", "noAvatarSelected": "Ningún personaje seleccionado",
"noAvatarToCopySelected": "Ningún personaje seleccionado para copiar", "noAvatarToCopySelected": "Ningún personaje seleccionado para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil", "pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil",
"pleaseEnterUid": "Por favor, introduce UID", "pleaseEnterUid": "Por favor, introduce UID",
"failedToFetchEnkaData": "Fallo al obtener datos de Enka", "failedToFetchEnkaData": "Fallo al obtener datos de Enka",
"pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje", "pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje",
"noDataToImport": "Sin datos para importar", "noDataToImport": "Sin datos para importar",
"pleaseSelectAFile": "Por favor, selecciona un archivo", "pleaseSelectAFile": "Por favor, selecciona un archivo",
"fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido", "fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido",
"importEnkaDataSuccess": "Datos de Enka importados con éxito", "importEnkaDataSuccess": "Datos de Enka importados con éxito",
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito", "importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
"importDatabaseSuccess": "Base de datos importada con éxito", "importDatabaseSuccess": "Base de datos importada con éxito",
"getData": "Obtener Datos", "getData": "Obtener Datos",
"import": "Importar", "import": "Importar",
"freeSRImport": "Importar FreeSR", "freeSRImport": "Importar FreeSR",
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR", "onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
"pickAFile": "Elegir un archivo", "pickAFile": "Elegir un archivo",
"lightConeSetting": "Ajuste de Cono de Luz", "lightConeSetting": "Ajuste de Cono de Luz",
"relicMaker": "Creador de Artefactos", "relicMaker": "Creador de Artefactos",
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones", "pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
"relicSavedSuccessfully": "Artefacto guardado exitosamente", "relicSavedSuccessfully": "Artefacto guardado exitosamente",
"mainSettings": "Ajustes Principales", "mainSettings": "Ajustes Principales",
"mainStat": "Estadística Principal", "mainStat": "Estadística Principal",
"set": "Conjunto", "set": "Conjunto",
"pleaseSelectASet": "Por favor, selecciona un conjunto", "pleaseSelectASet": "Por favor, selecciona un conjunto",
"effectBonus": "Bono de Efecto", "effectBonus": "Bono de Efecto",
"totalRoll": "Total de Mejoras", "totalRoll": "Total de Mejoras",
"randomizeStats": "Estadísticas Aleatorias", "randomizeStats": "Estadísticas Aleatorias",
"randomizeRolls": "Mejoras Aleatorias", "randomizeRolls": "Mejoras Aleatorias",
"selectASubStat": "Seleccionar estadística secundaria", "selectASubStat": "Seleccionar estadística secundaria",
"selectASet": "Seleccionar conjunto", "selectASet": "Seleccionar conjunto",
"selectAMainStat": "Seleccionar estadística principal", "selectAMainStat": "Seleccionar estadística principal",
"save": "Guardar", "save": "Guardar",
"reset": "Reiniciar", "reset": "Reiniciar",
"roll": "Mejora", "roll": "Mejora",
"step": "Paso", "step": "Paso",
"memoryOfChaos": "Memoria del Caos", "memoryOfChaos": "Memoria del Caos",
"pureFiction": "Pura Ficción", "pureFiction": "Pura Ficción",
"apocalypticShadow": "Sombra Apocalíptica", "apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Enemigo Personalizado", "customEnemy": "Enemigo Personalizado",
"simulatedUniverse": "Universo Simulado", "simulatedUniverse": "Universo Simulado",
"floor": "Piso", "floor": "Piso",
"side": "Lado", "side": "Lado",
"wave": "Oleada", "wave": "Oleada",
"stage": "Etapa", "stage": "Etapa",
"useCycleCount": "¿Usar conteo de ciclos?", "useCycleCount": "¿Usar conteo de ciclos?",
"useTurbulenceBuff": "¿Usar buff de turbulencia?", "useTurbulenceBuff": "¿Usar buff de turbulencia?",
"firstHalfEnemies": "Enemigos primera mitad", "firstHalfEnemies": "Enemigos primera mitad",
"secondHalfEnemies": "Enemigos segunda mitad", "secondHalfEnemies": "Enemigos segunda mitad",
"firstNodeEnemies": "Enemigos del nodo 1", "firstNodeEnemies": "Enemigos del nodo 1",
"secondNodeEnemies": "Enemigos del nodo 2", "secondNodeEnemies": "Enemigos del nodo 2",
"thirdNodeEnemies": "Enemigos del nodo 3", "thirdNodeEnemies": "Enemigos del nodo 3",
"firstNode": "Nodo 1", "firstNode": "Nodo 1",
"secondNode": "Nodo 2", "secondNode": "Nodo 2",
"thirdNode": "Nodo 3", "thirdNode": "Nodo 3",
"listEnemies": "Lista de enemigos", "listEnemies": "Lista de enemigos",
"turbulenceBuff": "Buff de Turbulencia", "turbulenceBuff": "Buff de Turbulencia",
"noEventSelected": "Ningún evento seleccionado", "noEventSelected": "Ningún evento seleccionado",
"noTurbulenceBuff": "Sin Buff de Turbulencia", "noTurbulenceBuff": "Sin Buff de Turbulencia",
"upper": "Superior", "upper": "Superior",
"lower": "Inferior", "lower": "Inferior",
"upperToLower": "Superior -> Inferior", "upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior", "lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Seleccionar evento de MOC", "selectMOCEvent": "Seleccionar evento de MOC",
"selectPFEvent": "Seleccionar evento de PF", "selectPFEvent": "Seleccionar evento de PF",
"selectASEvent": "Seleccionar evento de AS", "selectASEvent": "Seleccionar evento de AS",
"selectCEEvent": "Seleccionar evento de CE", "selectCEEvent": "Seleccionar evento de CE",
"selectEvent": "Seleccionar Evento", "selectEvent": "Seleccionar Evento",
"selectFloor": "Seleccionar Piso", "selectFloor": "Seleccionar Piso",
"selectSide": "Seleccionar Lado", "selectSide": "Seleccionar Lado",
"selectBuff": "Seleccionar Buff", "selectBuff": "Seleccionar Buff",
"selectStage": "Seleccionar Etapa", "selectStage": "Seleccionar Etapa",
"previous": "Anterior", "previous": "Anterior",
"next": "Siguiente", "next": "Siguiente",
"noMonstersFound": "No se encontraron monstruos", "noMonstersFound": "No se encontraron monstruos",
"addNewWave": "Añadir Nueva Oleada", "addNewWave": "Añadir Nueva Oleada",
"searchStage": "Buscar etapa...", "searchStage": "Buscar etapa...",
"noStageFound": "No se encontró etapa", "noStageFound": "No se encontró etapa",
"searchMonster": "Buscar monstruo...", "searchMonster": "Buscar monstruo...",
"changeRelic": "Cambiar artefacto", "changeRelic": "Cambiar artefacto",
"deleteRelic": "Borrar artefacto", "deleteRelic": "Borrar artefacto",
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?", "deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
"setEffects": "Configurar Efectos", "setEffects": "Configurar Efectos",
"details": "Detalles", "details": "Detalles",
"normal": "Ataque Básico", "normal": "Ataque Básico",
"bpskill": "Habilidad", "bpskill": "Habilidad",
"maze": "Técnica", "maze": "Técnica",
"ultra": "Habilidad Definitiva", "ultra": "Habilidad Definitiva",
"servantskill": "Habilidad de Mnemoduende", "assist": "Habilidad de asistencia",
"severaltalent": "Talento de Mnemoduende", "servantskill": "Habilidad de Mnemoduende",
"singleattack": "Ataque Individual", "severaltalent": "Talento de Mnemoduende",
"enhance": "Mejorar", "singleattack": "Ataque Individual",
"summon": "Invocar", "enhance": "Mejorar",
"mazeattack": "Ataque de Técnica", "summon": "Invocar",
"blast": "Ráfaga", "mazeattack": "Ataque de Técnica",
"restore": "Restaurar", "blast": "Ráfaga",
"support": "Soporte", "restore": "Restaurar",
"aoeattack": "Ataque en Área", "support": "Soporte",
"impair": "Debilitación", "aoeattack": "Ataque en Área",
"bounce": "Rebote", "impair": "Debilitación",
"active": "Activo", "bounce": "Rebote",
"defence": "Defensa", "active": "Activo",
"inactive": "Inactivo", "defence": "Defensa",
"maxAll": "Maximizar Todo", "inactive": "Inactivo",
"maxAllSuccess": "Habilidades maximizadas con éxito.", "maxAll": "Maximizar Todo",
"maxAllFailed": "Fallo al maximizar habilidades.", "maxAllSuccess": "Habilidades maximizadas con éxito.",
"noRelicEquipped": "Sin artefacto equipado", "maxAllFailed": "Fallo al maximizar habilidades.",
"anomalyArbitration": "Arbitraje Atípico", "noRelicEquipped": "Sin artefacto equipado",
"normalMode": "Modo Normal", "anomalyArbitration": "Arbitraje Atípico",
"hardMode": "Modo Difícil", "normalMode": "Modo Normal",
"selectPEAKEvent": "Seleccionar evento PEAK", "hardMode": "Modo Difícil",
"mode": "Modo", "selectPEAKEvent": "Seleccionar evento PEAK",
"selectMode": "Seleccionar un modo", "mode": "Modo",
"rollBack": "Retroceder", "selectMode": "Seleccionar un modo",
"upRoll": "Subir Mejora", "rollBack": "Retroceder",
"downRoll": "Bajar Mejora", "upRoll": "Subir Mejora",
"actions": "Acciones", "downRoll": "Bajar Mejora",
"avatars": "Avatares", "actions": "Acciones",
"quickView": "Vista Rápida", "avatars": "Avatares",
"extraSetting": "Ajustes Extra", "quickView": "Vista Rápida",
"disableCensorship": "Desactivar Censura", "extraSetting": "Ajustes Extra",
"hideUI": "Ocultar UI", "disableCensorship": "Desactivar Censura",
"theoryCraftMode": "Modo Theorycraft", "hideUI": "Ocultar UI",
"cycleCount": "Conteo de Ciclos", "theoryCraftMode": "Modo Theorycraft",
"pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas", "cycleCount": "Conteo de Ciclos",
"subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero", "pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero",
"multipathCharacter": "Personaje Multivía", "theoryCraft": "Theorycraft",
"mainPath": "Vía Principal", "multipathCharacter": "Personaje Multivía",
"march7Path": "Vía de Siete de Marzo", "mainPath": "Vía Principal",
"challenge": "Desafío", "march7Path": "Vía de Siete de Marzo",
"skipNode": "Saltar Nodo", "challenge": "Desafío",
"disableSkip": "Desactivar salto", "skipNode": "Saltar Nodo",
"skipNode1": "Saltar nodo 1", "disableSkip": "Desactivar salto",
"skipNode2": "Saltar nodo 2", "skipNode1": "Saltar nodo 1",
"extraFeatures": "Características Extra", "skipNode2": "Saltar nodo 2",
"detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.", "extraFeatures": "Características Extra",
"detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.", "detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.",
"detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.", "detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.",
"detailHiddenUi": "Oculta la interfaz del juego.", "detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.",
"detailDisableCensorship": "Desactiva la censura dentro del juego.", "detailHiddenUi": "Oculta la interfaz del juego.",
"detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.", "detailDisableCensorship": "Desactiva la censura dentro del juego.",
"trailblazer": "Trazacaminos", "detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.",
"listExtraEffect": "Lista de Efectos Extra", "trailblazer": "Trazacaminos",
"extra": "Extra", "listExtraEffect": "Lista de Efectos Extra",
"customLineup": "Alineación de equipo personalizada" "extra": "Extra",
} "customLineup": "Alineación de equipo personalizada",
} "def": "DEF",
"language": "Idioma"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Outils Firefly par Firefly Shelter" "description": "Outils Firefly par Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Type de Compétence", "skillType": "Type de Compétence",
"skillName": "Nom de Compétence", "skillName": "Nom de Compétence",
"character": "Personnage", "character": "Personnage",
"id": "Id", "id": "Id",
"path": "Voie", "path": "Voie",
"rarity": "Rareté", "rarity": "Rareté",
"element": "Élément", "element": "Élément",
"technique": "Technique", "technique": "Technique",
"talent": "Talent", "talent": "Talent",
"basic": "Attaque Normale", "basic": "Attaque Normale",
"skill": "Compétence", "skill": "Compétence",
"ultimate": "Ultime", "ultimate": "Ultime",
"servant": "Serviteur", "servant": "Serviteur",
"damage": "Dégâts", "damage": "Dégâts",
"type": "Type", "type": "Type",
"warrior": "La Destruction", "warrior": "La Destruction",
"knight": "La Préservation", "knight": "La Préservation",
"mage": "L'Érudition", "mage": "L'Érudition",
"priest": "L'Abondance", "priest": "L'Abondance",
"rogue": "La Chasse", "rogue": "La Chasse",
"shaman": "L'Harmonie", "shaman": "L'Harmonie",
"warlock": "La Nihilité", "warlock": "La Nihilité",
"memory": "Le Souvenir", "memory": "Le Souvenir",
"elation": "L'Allégresse", "elation": "L'Allégresse",
"fire": "Feu", "fire": "Feu",
"ice": "Glace", "ice": "Glace",
"imaginary": "Imaginaire", "imaginary": "Imaginaire",
"physical": "Physique", "physical": "Physique",
"quantum": "Quantique", "quantum": "Quantique",
"thunder": "Foudre", "thunder": "Foudre",
"wind": "Vent", "wind": "Vent",
"hp": "PV", "hp": "PV",
"atk": "ATQ", "atk": "ATQ",
"speed": "VIT", "speed": "VIT",
"critRate": "Taux CRIT", "critRate": "Taux CRIT",
"critDmg": "DGT CRIT", "critDmg": "DGT CRIT",
"breakEffect": "Effet de Rupture", "breakEffect": "Effet de Rupture",
"effectRes": "RÉS aux effets", "effectRes": "RÉS aux effets",
"energyRegenerationRate": "Taux de régénération d'énergie", "energyRegenerationRate": "Taux de régénération d'énergie",
"effectHitRate": "Chances d'application d'effets", "effectHitRate": "Chances d'application d'effets",
"outgoingHealingBoost": "Augmentation des soins", "outgoingHealingBoost": "Augmentation des soins",
"fireDmgBoost": "Bonus de dégâts de Feu", "fireDmgBoost": "Bonus de dégâts de Feu",
"iceDmgBoost": "Bonus de dégâts de Glace", "iceDmgBoost": "Bonus de dégâts de Glace",
"imaginaryDmgBoost": "Bonus de dégâts Imaginaire", "imaginaryDmgBoost": "Bonus de dégâts Imaginaire",
"physicalDmgBoost": "Bonus de dégâts Physique", "physicalDmgBoost": "Bonus de dégâts Physique",
"quantumDmgBoost": "Bonus de dégâts Quantique", "quantumDmgBoost": "Bonus de dégâts Quantique",
"thunderDmgBoost": "Bonus de dégâts de Foudre", "thunderDmgBoost": "Bonus de dégâts de Foudre",
"windDmgBoost": "Bonus de dégâts de Vent", "windDmgBoost": "Bonus de dégâts de Vent",
"pursued": "Dégâts additionnels", "pursued": "Dégâts additionnels",
"true damage": "Dégâts bruts", "true damage": "Dégâts bruts",
"elationdamage": "Dégâts d'Allégresse", "elationdamage": "Dégâts d'Allégresse",
"follow-up": "Dégâts d'attaque de suivi", "follow-up": "Dégâts d'attaque de suivi",
"elemental damage": "Dégâts de Rupture et Super Rupture", "elemental damage": "Dégâts de Rupture et Super Rupture",
"dot": "Dégâts sur la durée", "dot": "Dégâts sur la durée",
"qte": "Compétence QTE", "qte": "Compétence QTE",
"level": "Niveau", "level": "Niveau",
"relics": "Reliques", "relics": "Reliques",
"eidolons": "Eidolons", "eidolons": "Eidolons",
"lightcones": "Cônes de Lumière", "lightcones": "Cônes de Lumière",
"loadData": "Charger les données", "loadData": "Charger les données",
"exportData": "Exporter les données", "exportData": "Exporter les données",
"connectSetting": "Paramètres de connexion", "connectSetting": "Paramètres de connexion",
"connected": "Connecté", "connected": "Connecté",
"unconnected": "Déconnecté", "unconnected": "Déconnecté",
"psConnection": "Connexion PS", "psConnection": "Connexion PS",
"connectionType": "Type de connexion", "connectionType": "Type de connexion",
"status": "Statut", "status": "Statut",
"connectPs": "Connecter PS", "connectPs": "Connecter PS",
"disconnect": "Déconnecter", "disconnect": "Déconnecter",
"other": "Autre", "other": "Autre",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Base de données", "database": "Base de données",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Paramètre de monstres", "monsterSetting": "Paramètre de monstres",
"serverUrl": "URL du Serveur", "serverUrl": "URL du Serveur",
"privateType": "Type privé", "privateType": "Type privé",
"local": "Local", "local": "Local",
"server": "Serveur", "server": "Serveur",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"placeholderServerUrl": "Entrer l'URL du serveur", "placeholderServerUrl": "Entrer l'URL du serveur",
"placeholderUsername": "Entrer le nom d'utilisateur", "placeholderUsername": "Entrer le nom d'utilisateur",
"placeholderPassword": "Entrer le mot de passe", "placeholderPassword": "Entrer le mot de passe",
"connectedSuccess": "Connecté au PS avec succès", "connectedSuccess": "Connecté au PS avec succès",
"connectedFailed": "Échec de connexion au PS", "connectedFailed": "Échec de connexion au PS",
"syncSuccess": "Données synchronisées au PS avec succès", "syncSuccess": "Données synchronisées au PS avec succès",
"syncFailed": "Échec de synchronisation au PS", "syncFailed": "Échec de synchronisation au PS",
"sync": "Synchroniser", "sync": "Synchroniser",
"importSetting": "Paramètres d'importation", "importSetting": "Paramètres d'importation",
"profile": "Profil", "profile": "Profil",
"default": "Défaut", "default": "Défaut",
"copyProfiles": "Copier les profils", "copyProfiles": "Copier les profils",
"addNewProfile": "Ajouter un nouveau profil", "addNewProfile": "Ajouter un nouveau profil",
"createNewProfile": "Créer un nouveau profil", "createNewProfile": "Créer un nouveau profil",
"editProfile": "Éditer le profil", "editProfile": "Éditer le profil",
"placeholderProfileName": "Entrer le nom du profil", "placeholderProfileName": "Entrer le nom du profil",
"profileName": "Nom du profil", "profileName": "Nom du profil",
"create": "Créer", "create": "Créer",
"update": "Mettre à jour", "update": "Mettre à jour",
"characterInformation": "Informations du personnage", "characterInformation": "Informations du personnage",
"skills": "Compétences", "skills": "Compétences",
"showcaseCard": "Carte de présentation", "showcaseCard": "Carte de présentation",
"comingSoon": "Bientôt disponible", "comingSoon": "Bientôt disponible",
"characterName": "Nom du personnage", "characterName": "Nom du personnage",
"placeholderCharacter": "Entrer le nom du personnage", "placeholderCharacter": "Entrer le nom du personnage",
"characterSettings": "Paramètres du personnage", "characterSettings": "Paramètres du personnage",
"levelConfiguration": "Configuration de niveau", "levelConfiguration": "Configuration de niveau",
"characterLevel": "Niveau du personnage", "characterLevel": "Niveau du personnage",
"max": "MAX", "max": "MAX",
"ultimateEnergy": "Énergie de l'Ultime", "ultimateEnergy": "Énergie de l'Ultime",
"currentEnergy": "Énergie actuelle", "currentEnergy": "Énergie actuelle",
"setTo50": "Régler sur 50%", "setTo50": "Régler sur 50%",
"battleConfiguration": "Configuration de combat", "battleConfiguration": "Configuration de combat",
"useTechnique": "Utiliser Technique", "useTechnique": "Utiliser Technique",
"techniqueNote": "Activer les effets de technique avant le combat", "techniqueNote": "Activer les effets de technique avant le combat",
"enhancement": "Amélioration", "enhancement": "Amélioration",
"enhancementLevel": "Niveau d'amélioration", "enhancementLevel": "Niveau d'amélioration",
"origin": "Origine", "origin": "Origine",
"enhancedNote": "Les améliorations élevées débloquent des compétences", "enhancedNote": "Les améliorations élevées débloquent des compétences",
"lightconeEquipment": "Équipement de Cône de Lumière", "lightconeEquipment": "Équipement de Cône de Lumière",
"lightconeSettings": "Paramètres de Cône de Lumière", "lightconeSettings": "Paramètres de Cône de Lumière",
"placeholderLevel": "Entrer le niveau", "placeholderLevel": "Entrer le niveau",
"superimpositionRank": "Rang de Superposition", "superimpositionRank": "Rang de Superposition",
"ranksNote": "Les rangs élevés offrent des effets plus forts", "ranksNote": "Les rangs élevés offrent des effets plus forts",
"changeLightcone": "Changer de Cône de Lumière", "changeLightcone": "Changer de Cône de Lumière",
"removeLightcone": "Retirer le Cône de Lumière", "removeLightcone": "Retirer le Cône de Lumière",
"equipLightcone": "Équiper un Cône de Lumière", "equipLightcone": "Équiper un Cône de Lumière",
"noLightconeEquipped": "Aucun Cône de Lumière", "noLightconeEquipped": "Aucun Cône de Lumière",
"equipLightconeNote": "Équipez un cône pour renforcer votre personnage", "equipLightconeNote": "Équipez un cône pour renforcer votre personnage",
"filter": "Filtre", "filter": "Filtre",
"selectedCharacters": "Personnages sélectionnés", "selectedCharacters": "Personnages sélectionnés",
"selectedProfiles": "Profils sélectionnés", "selectedProfiles": "Profils sélectionnés",
"clearAll": "Tout effacer", "clearAll": "Tout effacer",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"copy": "Copier", "copy": "Copier",
"copied": "Copié", "copied": "Copié",
"noAvatarSelected": "Aucun personnage sélectionné", "noAvatarSelected": "Aucun personnage sélectionné",
"noAvatarToCopySelected": "Aucun personnage à copier sélectionné", "noAvatarToCopySelected": "Aucun personnage à copier sélectionné",
"pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil", "pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil",
"pleaseEnterUid": "Veuillez entrer un UID", "pleaseEnterUid": "Veuillez entrer un UID",
"failedToFetchEnkaData": "Échec de récupération des données Enka", "failedToFetchEnkaData": "Échec de récupération des données Enka",
"pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage", "pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage",
"noDataToImport": "Aucune donnée à importer", "noDataToImport": "Aucune donnée à importer",
"pleaseSelectAFile": "Veuillez sélectionner un fichier", "pleaseSelectAFile": "Veuillez sélectionner un fichier",
"fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide", "fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide",
"importEnkaDataSuccess": "Importation Enka réussie", "importEnkaDataSuccess": "Importation Enka réussie",
"importFreeSRDataSuccess": "Importation FreeSR réussie", "importFreeSRDataSuccess": "Importation FreeSR réussie",
"importDatabaseSuccess": "Importation de base de données réussie", "importDatabaseSuccess": "Importation de base de données réussie",
"getData": "Obtenir des données", "getData": "Obtenir des données",
"import": "Importer", "import": "Importer",
"freeSRImport": "Importation FreeSR", "freeSRImport": "Importation FreeSR",
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR", "onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
"pickAFile": "Choisir un fichier", "pickAFile": "Choisir un fichier",
"lightConeSetting": "Paramètre de Cône de Lumière", "lightConeSetting": "Paramètre de Cône de Lumière",
"relicMaker": "Créateur de Reliques", "relicMaker": "Créateur de Reliques",
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options", "pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
"relicSavedSuccessfully": "Relique sauvegardée avec succès", "relicSavedSuccessfully": "Relique sauvegardée avec succès",
"mainSettings": "Paramètres principaux", "mainSettings": "Paramètres principaux",
"mainStat": "Statistique principale", "mainStat": "Statistique principale",
"set": "Set", "set": "Set",
"pleaseSelectASet": "Veuillez sélectionner un set", "pleaseSelectASet": "Veuillez sélectionner un set",
"effectBonus": "Bonus d'effet", "effectBonus": "Bonus d'effet",
"totalRoll": "Total d'améliorations", "totalRoll": "Total d'améliorations",
"randomizeStats": "Statistiques aléatoires", "randomizeStats": "Statistiques aléatoires",
"randomizeRolls": "Améliorations aléatoires", "randomizeRolls": "Améliorations aléatoires",
"selectASubStat": "Sélectionner une stat secondaire", "selectASubStat": "Sélectionner une stat secondaire",
"selectASet": "Sélectionner un set", "selectASet": "Sélectionner un set",
"selectAMainStat": "Sélectionner une stat principale", "selectAMainStat": "Sélectionner une stat principale",
"save": "Sauvegarder", "save": "Sauvegarder",
"reset": "Réinitialiser", "reset": "Réinitialiser",
"roll": "Amélioration", "roll": "Amélioration",
"step": "Étape", "step": "Étape",
"memoryOfChaos": "Mémoire du Chaos", "memoryOfChaos": "Mémoire du Chaos",
"pureFiction": "Pure Fiction", "pureFiction": "Pure Fiction",
"apocalypticShadow": "Ombre Apocalyptique", "apocalypticShadow": "Ombre Apocalyptique",
"customEnemy": "Ennemi personnalisé", "customEnemy": "Ennemi personnalisé",
"simulatedUniverse": "Univers Simulé", "simulatedUniverse": "Univers Simulé",
"floor": "Étage", "floor": "Étage",
"side": "Côté", "side": "Côté",
"wave": "Vague", "wave": "Vague",
"stage": "Niveau", "stage": "Niveau",
"useCycleCount": "Utiliser le compteur de cycles ?", "useCycleCount": "Utiliser le compteur de cycles ?",
"useTurbulenceBuff": "Utiliser le buff de turbulence ?", "useTurbulenceBuff": "Utiliser le buff de turbulence ?",
"firstHalfEnemies": "Ennemis première moitié", "firstHalfEnemies": "Ennemis première moitié",
"secondHalfEnemies": "Ennemis deuxième moitié", "secondHalfEnemies": "Ennemis deuxième moitié",
"firstNodeEnemies": "Ennemis du nœud 1", "firstNodeEnemies": "Ennemis du nœud 1",
"secondNodeEnemies": "Ennemis du nœud 2", "secondNodeEnemies": "Ennemis du nœud 2",
"thirdNodeEnemies": "Ennemis du nœud 3", "thirdNodeEnemies": "Ennemis du nœud 3",
"firstNode": "Nœud 1", "firstNode": "Nœud 1",
"secondNode": "Nœud 2", "secondNode": "Nœud 2",
"thirdNode": "Nœud 3", "thirdNode": "Nœud 3",
"listEnemies": "Liste des ennemis", "listEnemies": "Liste des ennemis",
"turbulenceBuff": "Buff de Turbulence", "turbulenceBuff": "Buff de Turbulence",
"noEventSelected": "Aucun événement sélectionné", "noEventSelected": "Aucun événement sélectionné",
"noTurbulenceBuff": "Aucun Buff de Turbulence", "noTurbulenceBuff": "Aucun Buff de Turbulence",
"upper": "Supérieur", "upper": "Supérieur",
"lower": "Inférieur", "lower": "Inférieur",
"upperToLower": "Supérieur -> Inférieur", "upperToLower": "Supérieur -> Inférieur",
"lowerToUpper": "Inférieur -> Supérieur", "lowerToUpper": "Inférieur -> Supérieur",
"selectMOCEvent": "Sélectionner un événement MOC", "selectMOCEvent": "Sélectionner un événement MOC",
"selectPFEvent": "Sélectionner un événement PF", "selectPFEvent": "Sélectionner un événement PF",
"selectASEvent": "Sélectionner un événement AS", "selectASEvent": "Sélectionner un événement AS",
"selectCEEvent": "Sélectionner un événement CE", "selectCEEvent": "Sélectionner un événement CE",
"selectEvent": "Sélectionner un événement", "selectEvent": "Sélectionner un événement",
"selectFloor": "Sélectionner un étage", "selectFloor": "Sélectionner un étage",
"selectSide": "Sélectionner un côté", "selectSide": "Sélectionner un côté",
"selectBuff": "Sélectionner un buff", "selectBuff": "Sélectionner un buff",
"selectStage": "Sélectionner un niveau", "selectStage": "Sélectionner un niveau",
"previous": "Précédent", "previous": "Précédent",
"next": "Suivant", "next": "Suivant",
"noMonstersFound": "Aucun monstre trouvé", "noMonstersFound": "Aucun monstre trouvé",
"addNewWave": "Ajouter une nouvelle vague", "addNewWave": "Ajouter une nouvelle vague",
"searchStage": "Chercher un niveau...", "searchStage": "Chercher un niveau...",
"noStageFound": "Aucun niveau trouvé", "noStageFound": "Aucun niveau trouvé",
"searchMonster": "Chercher un monstre...", "searchMonster": "Chercher un monstre...",
"changeRelic": "Changer de relique", "changeRelic": "Changer de relique",
"deleteRelic": "Supprimer la relique", "deleteRelic": "Supprimer la relique",
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?", "deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
"setEffects": "Définir les effets", "setEffects": "Définir les effets",
"details": "Détails", "details": "Détails",
"normal": "Attaque Normale", "normal": "Attaque Normale",
"bpskill": "Compétence", "bpskill": "Compétence",
"maze": "Technique", "maze": "Technique",
"ultra": "Ultime", "ultra": "Ultime",
"servantskill": "Compétence de Mémolutin", "assist": "Compétence d'assistance",
"severaltalent": "Talent de Mémolutin", "servantskill": "Compétence de Mémolutin",
"singleattack": "Attaque à cible unique", "severaltalent": "Talent de Mémolutin",
"enhance": "Renforcement", "singleattack": "Attaque à cible unique",
"summon": "Invocation", "enhance": "Renforcement",
"mazeattack": "Attaque de Technique", "summon": "Invocation",
"blast": "Diffusion", "mazeattack": "Attaque de Technique",
"restore": "Restauration", "blast": "Diffusion",
"support": "Soutien", "restore": "Restauration",
"aoeattack": "Attaque de zone", "support": "Soutien",
"impair": "Malus", "aoeattack": "Attaque de zone",
"bounce": "Rebond", "impair": "Malus",
"active": "Actif", "bounce": "Rebond",
"defence": "Défense", "active": "Actif",
"inactive": "Inactif", "defence": "Défense",
"maxAll": "Tout maximiser", "inactive": "Inactif",
"maxAllSuccess": "Compétences maximisées avec succès.", "maxAll": "Tout maximiser",
"maxAllFailed": "Échec de maximisation.", "maxAllSuccess": "Compétences maximisées avec succès.",
"noRelicEquipped": "Aucune relique équipée", "maxAllFailed": "Échec de maximisation.",
"anomalyArbitration": "Arbitrage d'Anomalie", "noRelicEquipped": "Aucune relique équipée",
"normalMode": "Mode Normal", "anomalyArbitration": "Arbitrage d'Anomalie",
"hardMode": "Mode Difficile", "normalMode": "Mode Normal",
"selectPEAKEvent": "Sélectionner un événement PEAK", "hardMode": "Mode Difficile",
"mode": "Mode", "selectPEAKEvent": "Sélectionner un événement PEAK",
"selectMode": "Sélectionner un mode", "mode": "Mode",
"rollBack": "Annuler l'étape", "selectMode": "Sélectionner un mode",
"upRoll": "Augmenter l'amélioration", "rollBack": "Annuler l'étape",
"downRoll": "Diminuer l'amélioration", "upRoll": "Augmenter l'amélioration",
"actions": "Actions", "downRoll": "Diminuer l'amélioration",
"avatars": "Avatars", "actions": "Actions",
"quickView": "Aperçu rapide", "avatars": "Avatars",
"extraSetting": "Paramètres supplémentaires", "quickView": "Aperçu rapide",
"disableCensorship": "Désactiver la censure", "extraSetting": "Paramètres supplémentaires",
"hideUI": "Cacher l'UI", "disableCensorship": "Désactiver la censure",
"theoryCraftMode": "Mode Theorycraft", "hideUI": "Cacher l'UI",
"cycleCount": "Nombre de cycles", "theoryCraftMode": "Mode Theorycraft",
"pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires", "cycleCount": "Nombre de cycles",
"subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro", "pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro",
"multipathCharacter": "Personnage Multi-Voies", "theoryCraft": "Theorycraft",
"mainPath": "Voie Principale", "multipathCharacter": "Personnage Multi-Voies",
"march7Path": "Voie de March 7th", "mainPath": "Voie Principale",
"challenge": "Défi", "march7Path": "Voie de March 7th",
"skipNode": "Passer le nœud", "challenge": "Défi",
"disableSkip": "Désactiver le passage", "skipNode": "Passer le nœud",
"skipNode1": "Passer le nœud 1", "disableSkip": "Désactiver le passage",
"skipNode2": "Passer le nœud 2", "skipNode1": "Passer le nœud 1",
"extraFeatures": "Fonctionnalités supplémentaires", "skipNode2": "Passer le nœud 2",
"detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.", "extraFeatures": "Fonctionnalités supplémentaires",
"detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.", "detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.",
"detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.", "detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.",
"detailHiddenUi": "Cachera l'interface du jeu.", "detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.",
"detailDisableCensorship": "Désactivera la censure du jeu.", "detailHiddenUi": "Cachera l'interface du jeu.",
"detailMultipathCharacter": "Permet de changer la Voie de certains personnages.", "detailDisableCensorship": "Désactivera la censure du jeu.",
"trailblazer": "Pionnier", "detailMultipathCharacter": "Permet de changer la Voie de certains personnages.",
"listExtraEffect": "Liste des effets supplémentaires", "trailblazer": "Pionnier",
"extra": "Extra", "listExtraEffect": "Liste des effets supplémentaires",
"customLineup": "Composition personnalisée" "extra": "Extra",
} "customLineup": "Composition personnalisée",
} "def": "DÉF",
"language": "Langue"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Alat Firefly oleh Firefly Shelter" "description": "Alat Firefly oleh Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Tipe Skill", "skillType": "Tipe Skill",
"skillName": "Nama Skill", "skillName": "Nama Skill",
"character": "Karakter", "character": "Karakter",
"id": "Id", "id": "Id",
"path": "Path", "path": "Path",
"rarity": "Rarity", "rarity": "Rarity",
"element": "Elemen", "element": "Elemen",
"technique": "Technique", "technique": "Technique",
"talent": "Talent", "talent": "Talent",
"basic": "Basic Attack", "basic": "Basic Attack",
"skill": "Skill", "skill": "Skill",
"ultimate": "Ultimate", "ultimate": "Ultimate",
"servant": "Servant", "servant": "Servant",
"damage": "Damage", "damage": "Damage",
"type": "Tipe", "type": "Tipe",
"warrior": "The Destruction", "warrior": "The Destruction",
"knight": "The Preservation", "knight": "The Preservation",
"mage": "The Erudition", "mage": "The Erudition",
"priest": "The Abundance", "priest": "The Abundance",
"rogue": "The Hunt", "rogue": "The Hunt",
"shaman": "The Harmony", "shaman": "The Harmony",
"warlock": "The Nihility", "warlock": "The Nihility",
"memory": "The Remembrance", "memory": "The Remembrance",
"elation": "The Elation", "elation": "The Elation",
"fire": "Fire", "fire": "Fire",
"ice": "Ice", "ice": "Ice",
"imaginary": "Imaginary", "imaginary": "Imaginary",
"physical": "Physical", "physical": "Physical",
"quantum": "Quantum", "quantum": "Quantum",
"thunder": "Lightning", "thunder": "Lightning",
"wind": "Wind", "wind": "Wind",
"hp": "HP", "hp": "HP",
"atk": "ATK", "atk": "ATK",
"speed": "SPD", "speed": "SPD",
"critRate": "CRIT Rate", "critRate": "CRIT Rate",
"critDmg": "CRIT DMG", "critDmg": "CRIT DMG",
"breakEffect": "Break Effect", "breakEffect": "Break Effect",
"effectRes": "Effect RES", "effectRes": "Effect RES",
"energyRegenerationRate": "Energy Regeneration Rate", "energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate", "effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost", "outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire DMG Boost", "fireDmgBoost": "Fire DMG Boost",
"iceDmgBoost": "Ice DMG Boost", "iceDmgBoost": "Ice DMG Boost",
"imaginaryDmgBoost": "Imaginary DMG Boost", "imaginaryDmgBoost": "Imaginary DMG Boost",
"physicalDmgBoost": "Physical DMG Boost", "physicalDmgBoost": "Physical DMG Boost",
"quantumDmgBoost": "Quantum DMG Boost", "quantumDmgBoost": "Quantum DMG Boost",
"thunderDmgBoost": "Lightning DMG Boost", "thunderDmgBoost": "Lightning DMG Boost",
"windDmgBoost": "Wind DMG Boost", "windDmgBoost": "Wind DMG Boost",
"pursued": "Additional Damage", "pursued": "Additional Damage",
"true damage": "True Damage", "true damage": "True Damage",
"elationdamage": "Elation Damage", "elationdamage": "Elation Damage",
"follow-up": "Follow-up Attack Damage", "follow-up": "Follow-up Attack Damage",
"elemental damage": "Break dan Super Break Damage", "elemental damage": "Break dan Super Break Damage",
"dot": "Damage Over Time", "dot": "Damage Over Time",
"qte": "QTE Skill", "qte": "QTE Skill",
"level": "Level", "level": "Level",
"relics": "Relic", "relics": "Relic",
"eidolons": "Eidolon", "eidolons": "Eidolon",
"lightcones": "Light Cone", "lightcones": "Light Cone",
"loadData": "Muat data", "loadData": "Muat data",
"exportData": "Ekspor data", "exportData": "Ekspor data",
"connectSetting": "Pengaturan Koneksi", "connectSetting": "Pengaturan Koneksi",
"connected": "Terhubung", "connected": "Terhubung",
"unconnected": "Tidak terhubung", "unconnected": "Tidak terhubung",
"psConnection": "Koneksi PS", "psConnection": "Koneksi PS",
"connectionType": "Tipe Koneksi", "connectionType": "Tipe Koneksi",
"status": "Status", "status": "Status",
"connectPs": "Hubungkan PS", "connectPs": "Hubungkan PS",
"disconnect": "Putuskan sambungan", "disconnect": "Putuskan sambungan",
"other": "Lainnya", "other": "Lainnya",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Database", "database": "Database",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Pengaturan Monster", "monsterSetting": "Pengaturan Monster",
"serverUrl": "URL Server", "serverUrl": "URL Server",
"privateType": "Tipe Private", "privateType": "Tipe Private",
"local": "Lokal", "local": "Lokal",
"server": "Server", "server": "Server",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"placeholderServerUrl": "Masukkan URL server", "placeholderServerUrl": "Masukkan URL server",
"placeholderUsername": "Masukkan username", "placeholderUsername": "Masukkan username",
"placeholderPassword": "Masukkan password", "placeholderPassword": "Masukkan password",
"connectedSuccess": "Berhasil terhubung ke PS", "connectedSuccess": "Berhasil terhubung ke PS",
"connectedFailed": "Gagal terhubung ke PS", "connectedFailed": "Gagal terhubung ke PS",
"syncSuccess": "Berhasil sinkronisasi data ke PS", "syncSuccess": "Berhasil sinkronisasi data ke PS",
"syncFailed": "Gagal sinkronisasi data ke PS", "syncFailed": "Gagal sinkronisasi data ke PS",
"sync": "Sinkronisasi", "sync": "Sinkronisasi",
"importSetting": "Pengaturan Impor", "importSetting": "Pengaturan Impor",
"profile": "Profil", "profile": "Profil",
"default": "Default", "default": "Default",
"copyProfiles": "Salin profil", "copyProfiles": "Salin profil",
"addNewProfile": "Tambah profil baru", "addNewProfile": "Tambah profil baru",
"createNewProfile": "Buat profil baru", "createNewProfile": "Buat profil baru",
"editProfile": "Edit profil", "editProfile": "Edit profil",
"placeholderProfileName": "Masukkan nama profil", "placeholderProfileName": "Masukkan nama profil",
"profileName": "Nama profil", "profileName": "Nama profil",
"create": "Buat", "create": "Buat",
"update": "Perbarui", "update": "Perbarui",
"characterInformation": "Informasi Karakter", "characterInformation": "Informasi Karakter",
"skills": "Skill", "skills": "Skill",
"showcaseCard": "Showcase Card", "showcaseCard": "Showcase Card",
"comingSoon": "Segera hadir", "comingSoon": "Segera hadir",
"characterName": "Nama Karakter", "characterName": "Nama Karakter",
"placeholderCharacter": "Masukkan nama karakter", "placeholderCharacter": "Masukkan nama karakter",
"characterSettings": "Pengaturan Karakter", "characterSettings": "Pengaturan Karakter",
"levelConfiguration": "Konfigurasi Level", "levelConfiguration": "Konfigurasi Level",
"characterLevel": "Level Karakter", "characterLevel": "Level Karakter",
"max": "MAX", "max": "MAX",
"ultimateEnergy": "Energy Ultimate", "ultimateEnergy": "Energy Ultimate",
"currentEnergy": "Energy Saat Ini", "currentEnergy": "Energy Saat Ini",
"setTo50": "Atur ke 50%", "setTo50": "Atur ke 50%",
"battleConfiguration": "Konfigurasi Pertempuran", "battleConfiguration": "Konfigurasi Pertempuran",
"useTechnique": "Gunakan Technique", "useTechnique": "Gunakan Technique",
"techniqueNote": "Aktifkan efek technique sebelum pertempuran", "techniqueNote": "Aktifkan efek technique sebelum pertempuran",
"enhancement": "Enhancement", "enhancement": "Enhancement",
"enhancementLevel": "Level Enhancement", "enhancementLevel": "Level Enhancement",
"origin": "Asli", "origin": "Asli",
"enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan", "enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan",
"lightconeEquipment": "Peralatan Light Cone", "lightconeEquipment": "Peralatan Light Cone",
"lightconeSettings": "Pengaturan Light Cone", "lightconeSettings": "Pengaturan Light Cone",
"placeholderLevel": "Masukkan level", "placeholderLevel": "Masukkan level",
"superimpositionRank": "Superimposition Rank", "superimpositionRank": "Superimposition Rank",
"ranksNote": "Rank lebih tinggi memberikan efek lebih kuat", "ranksNote": "Rank lebih tinggi memberikan efek lebih kuat",
"changeLightcone": "Ganti Light Cone", "changeLightcone": "Ganti Light Cone",
"removeLightcone": "Lepas Light Cone", "removeLightcone": "Lepas Light Cone",
"equipLightcone": "Pakai Light Cone", "equipLightcone": "Pakai Light Cone",
"noLightconeEquipped": "Tidak Ada Light Cone", "noLightconeEquipped": "Tidak Ada Light Cone",
"equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu", "equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu",
"filter": "Filter", "filter": "Filter",
"selectedCharacters": "Karakter Dipilih", "selectedCharacters": "Karakter Dipilih",
"selectedProfiles": "Profil Dipilih", "selectedProfiles": "Profil Dipilih",
"clearAll": "Bersihkan Semua", "clearAll": "Bersihkan Semua",
"selectAll": "Pilih Semua", "selectAll": "Pilih Semua",
"copy": "Salin", "copy": "Salin",
"copied": "Tersalin", "copied": "Tersalin",
"noAvatarSelected": "Tidak ada karakter dipilih", "noAvatarSelected": "Tidak ada karakter dipilih",
"noAvatarToCopySelected": "Tidak ada karakter untuk disalin", "noAvatarToCopySelected": "Tidak ada karakter untuk disalin",
"pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil", "pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil",
"pleaseEnterUid": "Silakan masukkan UID", "pleaseEnterUid": "Silakan masukkan UID",
"failedToFetchEnkaData": "Gagal mengambil data Enka", "failedToFetchEnkaData": "Gagal mengambil data Enka",
"pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter", "pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter",
"noDataToImport": "Tidak ada data untuk diimpor", "noDataToImport": "Tidak ada data untuk diimpor",
"pleaseSelectAFile": "Silakan pilih file", "pleaseSelectAFile": "Silakan pilih file",
"fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid", "fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid",
"importEnkaDataSuccess": "Impor data Enka berhasil", "importEnkaDataSuccess": "Impor data Enka berhasil",
"importFreeSRDataSuccess": "Impor data FreeSR berhasil", "importFreeSRDataSuccess": "Impor data FreeSR berhasil",
"importDatabaseSuccess": "Impor database berhasil", "importDatabaseSuccess": "Impor database berhasil",
"getData": "Dapatkan Data", "getData": "Dapatkan Data",
"import": "Impor", "import": "Impor",
"freeSRImport": "Impor FreeSR", "freeSRImport": "Impor FreeSR",
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR", "onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
"pickAFile": "Pilih sebuah file", "pickAFile": "Pilih sebuah file",
"lightConeSetting": "Pengaturan Light Cone", "lightConeSetting": "Pengaturan Light Cone",
"relicMaker": "Pembuat Relic", "relicMaker": "Pembuat Relic",
"pleaseSelectAllOptions": "Silakan pilih semua opsi", "pleaseSelectAllOptions": "Silakan pilih semua opsi",
"relicSavedSuccessfully": "Relic berhasil disimpan", "relicSavedSuccessfully": "Relic berhasil disimpan",
"mainSettings": "Pengaturan Utama", "mainSettings": "Pengaturan Utama",
"mainStat": "Statistik Utama", "mainStat": "Statistik Utama",
"set": "Set", "set": "Set",
"pleaseSelectASet": "Pilih sebuah set", "pleaseSelectASet": "Pilih sebuah set",
"effectBonus": "Bonus Efek", "effectBonus": "Bonus Efek",
"totalRoll": "Total Roll", "totalRoll": "Total Roll",
"randomizeStats": "Acak Statistik", "randomizeStats": "Acak Statistik",
"randomizeRolls": "Acak Roll", "randomizeRolls": "Acak Roll",
"selectASubStat": "Pilih sub stat", "selectASubStat": "Pilih sub stat",
"selectASet": "Pilih sebuah set", "selectASet": "Pilih sebuah set",
"selectAMainStat": "Pilih stat utama", "selectAMainStat": "Pilih stat utama",
"save": "Simpan", "save": "Simpan",
"reset": "Reset", "reset": "Reset",
"roll": "Roll", "roll": "Roll",
"step": "Langkah", "step": "Langkah",
"memoryOfChaos": "Memory of Chaos", "memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction", "pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow", "apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Musuh Kustom", "customEnemy": "Musuh Kustom",
"simulatedUniverse": "Simulated Universe", "simulatedUniverse": "Simulated Universe",
"floor": "Lantai", "floor": "Lantai",
"side": "Sisi", "side": "Sisi",
"wave": "Gelombang", "wave": "Gelombang",
"stage": "Tahap", "stage": "Tahap",
"useCycleCount": "Gunakan hitungan siklus?", "useCycleCount": "Gunakan hitungan siklus?",
"useTurbulenceBuff": "Gunakan buff turbulence?", "useTurbulenceBuff": "Gunakan buff turbulence?",
"firstHalfEnemies": "Musuh paruh pertama", "firstHalfEnemies": "Musuh paruh pertama",
"secondHalfEnemies": "Musuh paruh kedua", "secondHalfEnemies": "Musuh paruh kedua",
"firstNodeEnemies": "Musuh Node 1", "firstNodeEnemies": "Musuh Node 1",
"secondNodeEnemies": "Musuh Node 2", "secondNodeEnemies": "Musuh Node 2",
"thirdNodeEnemies": "Musuh Node 3", "thirdNodeEnemies": "Musuh Node 3",
"firstNode": "Node 1", "firstNode": "Node 1",
"secondNode": "Node 2", "secondNode": "Node 2",
"thirdNode": "Node 3", "thirdNode": "Node 3",
"listEnemies": "Daftar musuh", "listEnemies": "Daftar musuh",
"turbulenceBuff": "Turbulence Buff", "turbulenceBuff": "Turbulence Buff",
"noEventSelected": "Tidak ada event dipilih", "noEventSelected": "Tidak ada event dipilih",
"noTurbulenceBuff": "Tanpa Turbulence Buff", "noTurbulenceBuff": "Tanpa Turbulence Buff",
"upper": "Atas", "upper": "Atas",
"lower": "Bawah", "lower": "Bawah",
"upperToLower": "Atas -> Bawah", "upperToLower": "Atas -> Bawah",
"lowerToUpper": "Bawah -> Atas", "lowerToUpper": "Bawah -> Atas",
"selectMOCEvent": "Pilih Event MOC", "selectMOCEvent": "Pilih Event MOC",
"selectPFEvent": "Pilih Event PF", "selectPFEvent": "Pilih Event PF",
"selectASEvent": "Pilih Event AS", "selectASEvent": "Pilih Event AS",
"selectCEEvent": "Pilih Event CE", "selectCEEvent": "Pilih Event CE",
"selectEvent": "Pilih Event", "selectEvent": "Pilih Event",
"selectFloor": "Pilih Lantai", "selectFloor": "Pilih Lantai",
"selectSide": "Pilih Sisi", "selectSide": "Pilih Sisi",
"selectBuff": "Pilih Buff", "selectBuff": "Pilih Buff",
"selectStage": "Pilih Tahap", "selectStage": "Pilih Tahap",
"previous": "Sebelumnya", "previous": "Sebelumnya",
"next": "Selanjutnya", "next": "Selanjutnya",
"noMonstersFound": "Tidak ada monster ditemukan", "noMonstersFound": "Tidak ada monster ditemukan",
"addNewWave": "Tambah Gelombang Baru", "addNewWave": "Tambah Gelombang Baru",
"searchStage": "Cari tahap...", "searchStage": "Cari tahap...",
"noStageFound": "Tahap tidak ditemukan", "noStageFound": "Tahap tidak ditemukan",
"searchMonster": "Cari monster...", "searchMonster": "Cari monster...",
"changeRelic": "Ganti relic", "changeRelic": "Ganti relic",
"deleteRelic": "Hapus relic", "deleteRelic": "Hapus relic",
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?", "deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
"setEffects": "Atur Efek", "setEffects": "Atur Efek",
"details": "Detail", "details": "Detail",
"normal": "Basic ATK", "normal": "Basic ATK",
"bpskill": "Skill", "bpskill": "Skill",
"maze": "Technique", "maze": "Technique",
"ultra": "Ultimate", "ultra": "Ultimate",
"servantskill": "Skill Memosprite", "assist": "Skill Bantuan",
"severaltalent": "Talent Memosprite", "servantskill": "Skill Memosprite",
"singleattack": "Single Attack", "severaltalent": "Talent Memosprite",
"enhance": "Enhance", "singleattack": "Single Attack",
"summon": "Summon", "enhance": "Enhance",
"mazeattack": "Technique Attack", "summon": "Summon",
"blast": "Blast", "mazeattack": "Technique Attack",
"restore": "Restore", "blast": "Blast",
"support": "Support", "restore": "Restore",
"aoeattack": "AoE Attack", "support": "Support",
"impair": "Impair", "aoeattack": "AoE Attack",
"bounce": "Bounce", "impair": "Impair",
"active": "Aktif", "bounce": "Bounce",
"defence": "Defence", "active": "Aktif",
"inactive": "Nonaktif", "defence": "Defence",
"maxAll": "Maksimalkan Semua", "inactive": "Nonaktif",
"maxAllSuccess": "Berhasil memaksimalkan level skill.", "maxAll": "Maksimalkan Semua",
"maxAllFailed": "Gagal memaksimalkan level skill.", "maxAllSuccess": "Berhasil memaksimalkan level skill.",
"noRelicEquipped": "Tidak ada relic dipakai", "maxAllFailed": "Gagal memaksimalkan level skill.",
"anomalyArbitration": "Anomaly Arbitration", "noRelicEquipped": "Tidak ada relic dipakai",
"normalMode": "Mode Normal", "anomalyArbitration": "Anomaly Arbitration",
"hardMode": "Mode Sulit", "normalMode": "Mode Normal",
"selectPEAKEvent": "Pilih Event PEAK", "hardMode": "Mode Sulit",
"mode": "Mode", "selectPEAKEvent": "Pilih Event PEAK",
"selectMode": "Pilih Mode", "mode": "Mode",
"rollBack": "Roll Back", "selectMode": "Pilih Mode",
"upRoll": "Up Roll", "rollBack": "Roll Back",
"downRoll": "Down Roll", "upRoll": "Up Roll",
"actions": "Aksi", "downRoll": "Down Roll",
"avatars": "Avatar", "actions": "Aksi",
"quickView": "Tampilan Cepat", "avatars": "Avatar",
"extraSetting": "Pengaturan Ekstra", "quickView": "Tampilan Cepat",
"disableCensorship": "Nonaktifkan Sensor", "extraSetting": "Pengaturan Ekstra",
"hideUI": "Sembunyikan UI", "disableCensorship": "Nonaktifkan Sensor",
"theoryCraftMode": "Mode Theorycraft", "hideUI": "Sembunyikan UI",
"cycleCount": "Hitungan Siklus", "theoryCraftMode": "Mode Theorycraft",
"pleaseSelectAllSubStats": "Pilih semua sub statistik", "cycleCount": "Hitungan Siklus",
"subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol", "pleaseSelectAllSubStats": "Pilih semua sub statistik",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol",
"multipathCharacter": "Karakter Multipath", "theoryCraft": "Theorycraft",
"mainPath": "Path Utama", "multipathCharacter": "Karakter Multipath",
"march7Path": "Path March 7th", "mainPath": "Path Utama",
"challenge": "Tantangan", "march7Path": "Path March 7th",
"skipNode": "Lewati Node", "challenge": "Tantangan",
"disableSkip": "Nonaktifkan lewati", "skipNode": "Lewati Node",
"skipNode1": "Lewati node 1", "disableSkip": "Nonaktifkan lewati",
"skipNode2": "Lewati node 2", "skipNode1": "Lewati node 1",
"extraFeatures": "Fitur Ekstra", "skipNode2": "Lewati node 2",
"detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.", "extraFeatures": "Fitur Ekstra",
"detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.", "detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.",
"detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.", "detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.",
"detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.", "detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.",
"detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.", "detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.",
"detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.", "detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.",
"trailblazer": "Trailblazer", "detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.",
"listExtraEffect": "Daftar Efek Ekstra", "trailblazer": "Trailblazer",
"extra": "Ekstra", "listExtraEffect": "Daftar Efek Ekstra",
"customLineup": "Lineup Kustom" "extra": "Ekstra",
} "customLineup": "Lineup Kustom",
} "def": "DEF",
"language": "Bahasa"
}
}
+292 -289
View File
@@ -1,289 +1,292 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "スキルタイプ", "skillType": "スキルタイプ",
"skillName": "スキル名", "skillName": "スキル名",
"character": "キャラクター", "character": "キャラクター",
"id": "ID", "id": "ID",
"path": "運命", "path": "運命",
"rarity": "レアリティ", "rarity": "レアリティ",
"element": "属性", "element": "属性",
"technique": "秘技", "technique": "秘技",
"talent": "天賦", "talent": "天賦",
"basic": "通常攻撃", "basic": "通常攻撃",
"skill": "スキル", "skill": "スキル",
"ultimate": "アルティメット", "ultimate": "アルティメット",
"servant": "サーバント", "servant": "サーバント",
"damage": "ダメージ", "damage": "ダメージ",
"type": "タイプ", "type": "タイプ",
"warrior": "壊滅", "warrior": "壊滅",
"knight": "存護", "knight": "存護",
"mage": "知恵", "mage": "知恵",
"priest": "豊穣", "priest": "豊穣",
"rogue": "巡狩", "rogue": "巡狩",
"shaman": "調和", "shaman": "調和",
"warlock": "虚無", "warlock": "虚無",
"memory": "記憶", "memory": "記憶",
"elation": "愉悦", "elation": "愉悦",
"fire": "火", "fire": "火",
"ice": "氷", "ice": "氷",
"imaginary": "虚数", "imaginary": "虚数",
"physical": "物理", "physical": "物理",
"quantum": "量子", "quantum": "量子",
"thunder": "雷", "thunder": "雷",
"wind": "風", "wind": "風",
"hp": "HP", "hp": "HP",
"atk": "攻撃", "atk": "攻撃",
"speed": "速度", "speed": "速度",
"critRate": "会心率", "critRate": "会心率",
"critDmg": "会心ダメージ", "critDmg": "会心ダメージ",
"breakEffect": "撃破特効", "breakEffect": "撃破特効",
"effectRes": "効果抵抗", "effectRes": "効果抵抗",
"energyRegenerationRate": "EP回復効率", "energyRegenerationRate": "EP回復効率",
"effectHitRate": "効果命中率", "effectHitRate": "効果命中率",
"outgoingHealingBoost": "回復増加", "outgoingHealingBoost": "回復増加",
"fireDmgBoost": "火属性ダメージ強化", "fireDmgBoost": "火属性ダメージ強化",
"iceDmgBoost": "氷属性ダメージ強化", "iceDmgBoost": "氷属性ダメージ強化",
"imaginaryDmgBoost": "幻影ダメージ強化", "imaginaryDmgBoost": "幻影ダメージ強化",
"physicalDmgBoost": "物理ダメージ強化", "physicalDmgBoost": "物理ダメージ強化",
"quantumDmgBoost": "量子ダメージ強化", "quantumDmgBoost": "量子ダメージ強化",
"thunderDmgBoost": "雷属性ダメージ強化", "thunderDmgBoost": "雷属性ダメージ強化",
"windDmgBoost": "風属性ダメージ強化", "windDmgBoost": "風属性ダメージ強化",
"pursued": "追加ダメージ", "pursued": "追加ダメージ",
"true damage": "確定ダメージ", "true damage": "確定ダメージ",
"elationdamage": "愉悦ダメージ", "elationdamage": "愉悦ダメージ",
"follow-up": "追撃ダメージ", "follow-up": "追撃ダメージ",
"elemental damage": "属性ダメージ", "elemental damage": "属性ダメージ",
"dot": "継続ダメージ", "dot": "継続ダメージ",
"qte": "QTEスキル", "qte": "QTEスキル",
"level": "レベル", "level": "レベル",
"relics": "遺物", "relics": "遺物",
"eidolons": "エイドロン", "eidolons": "エイドロン",
"lightcones": "光円錐", "lightcones": "光円錐",
"loadData": "データ読み込み", "loadData": "データ読み込み",
"exportData": "データ出力", "exportData": "データ出力",
"connectSetting": "接続設定", "connectSetting": "接続設定",
"connected": "接続済み", "connected": "接続済み",
"unconnected": "未接続", "unconnected": "未接続",
"psConnection": "PS接続", "psConnection": "PS接続",
"connectionType": "接続タイプ", "connectionType": "接続タイプ",
"status": "ステータス", "status": "ステータス",
"connectPs": "PS接続", "connectPs": "PS接続",
"disconnect": "切断", "disconnect": "切断",
"other": "その他", "other": "その他",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "データベース", "database": "データベース",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "モンスター設定", "monsterSetting": "モンスター設定",
"serverUrl": "サーバー URL", "serverUrl": "サーバー URL",
"privateType": "プライベートタイプ", "privateType": "プライベートタイプ",
"local": "ローカル", "local": "ローカル",
"server": "サーバー", "server": "サーバー",
"username": "ユーザー名", "username": "ユーザー名",
"password": "パスワード", "password": "パスワード",
"placeholderServerUrl": "サーバー URL を入力", "placeholderServerUrl": "サーバー URL を入力",
"placeholderUsername": "ユーザー名を入力", "placeholderUsername": "ユーザー名を入力",
"placeholderPassword": "パスワードを入力", "placeholderPassword": "パスワードを入力",
"connectedSuccess": "PS接続成功", "connectedSuccess": "PS接続成功",
"connectedFailed": "PS接続失敗", "connectedFailed": "PS接続失敗",
"syncSuccess": "PSへのデータ同期に成功", "syncSuccess": "PSへのデータ同期に成功",
"syncFailed": "PSへのデータ同期に失敗", "syncFailed": "PSへのデータ同期に失敗",
"sync": "同期", "sync": "同期",
"importSetting": "インポート設定", "importSetting": "インポート設定",
"profile": "プロファイル", "profile": "プロファイル",
"default": "デフォルト", "default": "デフォルト",
"copyProfiles": "プロファイルをコピー", "copyProfiles": "プロファイルをコピー",
"addNewProfile": "プロファイルを追加", "addNewProfile": "プロファイルを追加",
"createNewProfile": "新しいプロファイルを作成", "createNewProfile": "新しいプロファイルを作成",
"editProfile": "プロファイル編集", "editProfile": "プロファイル編集",
"placeholderProfileName": "プロファイル名を入力", "placeholderProfileName": "プロファイル名を入力",
"profileName": "プロファイル名", "profileName": "プロファイル名",
"create": "作成", "create": "作成",
"update": "更新", "update": "更新",
"characterInformation": "キャラクター情報", "characterInformation": "キャラクター情報",
"skills": "スキル", "skills": "スキル",
"showcaseCard": "ショーケースカード", "showcaseCard": "ショーケースカード",
"comingSoon": "近日公開", "comingSoon": "近日公開",
"characterName": "キャラクター名", "characterName": "キャラクター名",
"placeholderCharacter": "キャラクター名を入力", "placeholderCharacter": "キャラクター名を入力",
"characterSettings": "キャラクター設定", "characterSettings": "キャラクター設定",
"levelConfiguration": "レベル構成", "levelConfiguration": "レベル構成",
"characterLevel": "キャラクターレベル", "characterLevel": "キャラクターレベル",
"max": "最大", "max": "最大",
"ultimateEnergy": "アルティメットエネルギー", "ultimateEnergy": "アルティメットエネルギー",
"currentEnergy": "現在のエネルギー", "currentEnergy": "現在のエネルギー",
"setTo50": "50%に設定", "setTo50": "50%に設定",
"battleConfiguration": "バトル構成", "battleConfiguration": "バトル構成",
"useTechnique": "秘技を使用", "useTechnique": "秘技を使用",
"techniqueNote": "バトル前に秘技効果を有効化", "techniqueNote": "バトル前に秘技効果を有効化",
"enhancement": "強化", "enhancement": "強化",
"enhancementLevel": "強化レベル", "enhancementLevel": "強化レベル",
"origin": "由来", "origin": "由来",
"enhancedNote": "強化レベルに応じて追加スキル解放", "enhancedNote": "強化レベルに応じて追加スキル解放",
"lightconeEquipment": "光円錐装備", "lightconeEquipment": "光円錐装備",
"lightconeSettings": "光円錐の設定", "lightconeSettings": "光円錐の設定",
"placeholderLevel": "レベルを入力", "placeholderLevel": "レベルを入力",
"superimpositionRank": "重ねランク", "superimpositionRank": "重ねランク",
"ranksNote": "ランクが高いほど効果強化", "ranksNote": "ランクが高いほど効果強化",
"changeLightcone": "光円錐を変更", "changeLightcone": "光円錐を変更",
"removeLightcone": "光円錐を外す", "removeLightcone": "光円錐を外す",
"equipLightcone": "光円錐を装備", "equipLightcone": "光円錐を装備",
"noLightconeEquipped": "光円錐が装備されていません", "noLightconeEquipped": "光円錐が装備されていません",
"equipLightconeNote": "強化のため光円錐を装備してください", "equipLightconeNote": "強化のため光円錐を装備してください",
"filter": "フィルター", "filter": "フィルター",
"selectedCharacters": "選択中のキャラクター", "selectedCharacters": "選択中のキャラクター",
"selectedProfiles": "選択中のプロファイル", "selectedProfiles": "選択中のプロファイル",
"clearAll": "全てクリア", "clearAll": "全てクリア",
"selectAll": "全て選択", "selectAll": "全て選択",
"copy": "コピー", "copy": "コピー",
"copied": "コピー済み", "copied": "コピー済み",
"noAvatarSelected": "キャラクターが選択されていません", "noAvatarSelected": "キャラクターが選択されていません",
"noAvatarToCopySelected": "コピーするキャラクター選択されていません", "noAvatarToCopySelected": "コピーするキャラクター選択されていません",
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください", "pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
"pleaseEnterUid": "UIDを入力してください", "pleaseEnterUid": "UIDを入力してください",
"failedToFetchEnkaData": "Enka のデータ取得に失敗", "failedToFetchEnkaData": "Enka のデータ取得に失敗",
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください", "pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
"noDataToImport": "インポートするデータがありません", "noDataToImport": "インポートするデータがありません",
"pleaseSelectAFile": "ファイルを選択してください", "pleaseSelectAFile": "ファイルを選択してください",
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります", "fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
"importEnkaDataSuccess": "Enka データのインポート成功", "importEnkaDataSuccess": "Enka データのインポート成功",
"importFreeSRDataSuccess": "FreeSR データのインポート成功", "importFreeSRDataSuccess": "FreeSR データのインポート成功",
"importDatabaseSuccess": "データベースのインポート成功", "importDatabaseSuccess": "データベースのインポート成功",
"getData": "データ取得", "getData": "データ取得",
"import": "インポート", "import": "インポート",
"freeSRImport": "FreeSR インポート", "freeSRImport": "FreeSR インポート",
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応", "onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
"pickAFile": "ファイル選択", "pickAFile": "ファイル選択",
"lightConeSetting": "ライトコーン設定", "lightConeSetting": "ライトコーン設定",
"relicMaker": "遺物製作", "relicMaker": "遺物製作",
"pleaseSelectAllOptions": "すべてのオプションを選択してください", "pleaseSelectAllOptions": "すべてのオプションを選択してください",
"relicSavedSuccessfully": "遺物の保存に成功しました", "relicSavedSuccessfully": "遺物の保存に成功しました",
"mainSettings": "メイン設定", "mainSettings": "メイン設定",
"mainStat": "メインステータス", "mainStat": "メインステータス",
"set": "セット", "set": "セット",
"pleaseSelectASet": "セットを選択してください", "pleaseSelectASet": "セットを選択してください",
"effectBonus": "効果ボーナス", "effectBonus": "効果ボーナス",
"totalRoll": "総ロール数", "totalRoll": "総ロール数",
"randomizeStats": "ステータスランダム化", "randomizeStats": "ステータスランダム化",
"randomizeRolls": "ロール回数ランダム化", "randomizeRolls": "ロール回数ランダム化",
"selectASubStat": "サブステータスを選択", "selectASubStat": "サブステータスを選択",
"selectASet": "セットを選択", "selectASet": "セットを選択",
"selectAMainStat": "メインステータスを選んでください", "selectAMainStat": "メインステータスを選んでください",
"save": "保存", "save": "保存",
"reset": "リセット", "reset": "リセット",
"roll": "ロール", "roll": "ロール",
"step": "ステップ", "step": "ステップ",
"memoryOfChaos": "混沌の記憶", "memoryOfChaos": "混沌の記憶",
"pureFiction": "虚構叙事", "pureFiction": "虚構叙事",
"apocalypticShadow": "末日の幻影", "apocalypticShadow": "末日の幻影",
"customEnemy": "カスタム敵", "customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙", "simulatedUniverse": "模擬宇宙",
"floor": "階層", "floor": "階層",
"side": "前半/後半", "side": "前半/後半",
"wave": "ウェーブ", "wave": "ウェーブ",
"stage": "ステージ", "stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?", "useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?", "useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵", "firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵", "secondHalfEnemies": "後半の敵",
"firstNodeEnemies": "ノード 1 の敵", "firstNodeEnemies": "ノード 1 の敵",
"secondNodeEnemies": "ノード 2 の敵", "secondNodeEnemies": "ノード 2 の敵",
"thirdNodeEnemies": "ノード 3 の敵", "thirdNodeEnemies": "ノード 3 の敵",
"firstNode": "ノード 1", "firstNode": "ノード 1",
"secondNode": "ノード 2", "secondNode": "ノード 2",
"thirdNode": "ノード 3", "thirdNode": "ノード 3",
"turbulenceBuff": "乱気流バフ", "turbulenceBuff": "乱気流バフ",
"noEventSelected": "イベントが選択されていません", "noEventSelected": "イベントが選択されていません",
"noTurbulenceBuff": "乱気流バフがありません", "noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半", "upper": "上半",
"lower": "下半", "lower": "下半",
"upperToLower": "上半 -> 下半", "upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半", "lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択", "selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択", "selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択", "selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択", "selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択", "selectEvent": "イベントを選択",
"selectFloor": "階層を選択", "selectFloor": "階層を選択",
"selectSide": "前半/後半を選択", "selectSide": "前半/後半を選択",
"selectBuff": "バフを選択", "selectBuff": "バフを選択",
"selectStage": "ステージを選択", "selectStage": "ステージを選択",
"previous": "前へ", "previous": "前へ",
"next": "次へ", "next": "次へ",
"noMonstersFound": "敵が見つかりません", "noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加", "addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...", "searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません", "noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...", "searchMonster": "敵を検索...",
"changeRelic": "遺物を変更", "changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除", "deleteRelic": "遺物を削除",
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?", "deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
"setEffects": "効果を設定", "setEffects": "効果を設定",
"details": "詳細", "details": "詳細",
"normal": "通常攻撃", "normal": "通常攻撃",
"bpskill": "戦闘スキル", "bpskill": "戦闘スキル",
"maze": "秘技", "assist": "アシストスキル",
"ultra": "必殺技", "maze": "秘技",
"servantskill": "メモスプライトスキル", "ultra": "必殺技",
"severaltalent": "メモスプライトの才能", "servantskill": "メモスプライトスキル",
"singleattack": "単体攻撃", "severaltalent": "メモスプライトの才能",
"enhance": "強化", "singleattack": "単体攻撃",
"summon": "召喚", "enhance": "強化",
"blast": "爆発", "summon": "召喚",
"restore": "回復", "blast": "爆発",
"support": "支援", "restore": "回復",
"aoeattack": "範囲攻撃", "support": "支援",
"mazeattack": "迷宮秘技", "aoeattack": "範囲攻撃",
"impair": "弱体化", "mazeattack": "迷宮秘技",
"bounce": "弾跳", "impair": "弱体化",
"active": "アクティブ", "bounce": "弾跳",
"inactive": "非アクティブ", "active": "アクティブ",
"defence": "防御", "inactive": "非アクティブ",
"maxAll": "すべて最大化", "defence": "防御",
"maxAllSuccess": "スキルレベルを最大に設定しました。", "maxAll": "すべて最大化",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。", "maxAllSuccess": "スキルレベルを最大に設定しました。",
"noRelicEquipped": "遺物が装備されていません", "maxAllFailed": "スキルレベルの最大設定に失敗しました。",
"anomalyArbitration": "異相の仲裁", "noRelicEquipped": "遺物が装備されていません",
"normalMode": "通常モード", "anomalyArbitration": "異相の仲裁",
"hardMode": "困難モード", "normalMode": "通常モード",
"selectPEAKEvent": "PEAK イベントを選択", "hardMode": "困難モード",
"mode": "モード", "selectPEAKEvent": "PEAK イベントを選択",
"selectMode": "モードを選択", "mode": "モード",
"rollBack": "前の状態に戻る", "selectMode": "モードを選択",
"upRoll": "サブステータスを増やす", "rollBack": "前の状態に戻る",
"downRoll": "サブステータスを減らす", "upRoll": "サブステータスを増やす",
"actions": "アクション", "downRoll": "サブステータスを減らす",
"avatars": "アバター", "actions": "アクション",
"quickView": "クイックビュー", "avatars": "アバター",
"extraSetting": "追加設定", "quickView": "クイックビュー",
"disableCensorship": "検閲を無効化", "extraSetting": "追加設定",
"hideUI": "UIを非表示", "disableCensorship": "検閲を無効化",
"theoryCraftMode": "シアリークラフトモード", "hideUI": "UIを非表示",
"cycleCount": "サイクル数", "theoryCraftMode": "シアリークラフトモード",
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください", "cycleCount": "サイクル数",
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません", "pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
"theoryCraft": "シアリークラフト", "subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
"multipathCharacter": "複数運命キャラ", "theoryCraft": "シアリークラフト",
"mainPath": "主人公の運命", "multipathCharacter": "複数運命キャラ",
"march7Path": "三月なのかの運命", "mainPath": "主人公の運命",
"challenge": "挑戦", "march7Path": "三月なのかの運命",
"skipNode": "ノードをスキップ", "challenge": "挑戦",
"disableSkip": "スキップ無効", "skipNode": "ノードをスキップ",
"skipNode1": "ノード1をスキップ", "disableSkip": "スキップ無効",
"skipNode2": "ノード2をスキップ", "skipNode1": "ノード1をスキップ",
"extraFeatures": "追加機能", "skipNode2": "ノード2をスキップ",
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。", "extraFeatures": "追加機能",
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。", "detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。", "detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。", "detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。", "detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。", "detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
"trailblazer": "開拓者", "detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
"listExtraEffect": "追加効果一覧", "trailblazer": "開拓者",
"extra": "追加", "listExtraEffect": "追加効果一覧",
"customLineup": "カスタム編成" "extra": "追加",
} "customLineup": "カスタム編成",
} "def": "防御力",
"language": "言語"
}
}
+292 -289
View File
@@ -1,289 +1,292 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "스킬 유형", "skillType": "스킬 유형",
"skillName": "스킬 이름", "skillName": "스킬 이름",
"character": "캐릭터", "character": "캐릭터",
"id": "ID", "id": "ID",
"path": "운명", "path": "운명",
"rarity": "희귀도", "rarity": "희귀도",
"element": "속성", "element": "속성",
"technique": "비기", "technique": "비기",
"talent": "탈렌트", "talent": "탈렌트",
"basic": "기본 공격", "basic": "기본 공격",
"skill": "스킬", "skill": "스킬",
"ultimate": "궁극기", "ultimate": "궁극기",
"servant": "서번트", "servant": "서번트",
"damage": "데미지", "damage": "데미지",
"type": "종류", "type": "종류",
"warrior": "파괴", "warrior": "파괴",
"knight": "보호", "knight": "보호",
"mage": "박학", "mage": "박학",
"priest": "풍요", "priest": "풍요",
"rogue": "사냥", "rogue": "사냥",
"shaman": "조화", "shaman": "조화",
"warlock": "허무", "warlock": "허무",
"memory": "기억", "memory": "기억",
"elation": "환락", "elation": "환락",
"fire": "화염", "fire": "화염",
"ice": "얼음", "ice": "얼음",
"imaginary": "환상", "imaginary": "환상",
"physical": "물리", "physical": "물리",
"quantum": "양자", "quantum": "양자",
"thunder": "번개", "thunder": "번개",
"wind": "바람", "wind": "바람",
"hp": "HP", "hp": "HP",
"atk": "공격력", "atk": "공격력",
"speed": "속도", "speed": "속도",
"critRate": "치명타 확률", "critRate": "치명타 확률",
"critDmg": "치명타 피해", "critDmg": "치명타 피해",
"breakEffect": "파괴 피해", "breakEffect": "파괴 피해",
"effectRes": "효과 저항", "effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복속도", "energyRegenerationRate": "에너지 회복속도",
"effectHitRate": "효과 적중률", "effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유 강화", "outgoingHealingBoost": "치유 강화",
"fireDmgBoost": "화염 피해 증가", "fireDmgBoost": "화염 피해 증가",
"iceDmgBoost": "얼음 피해 증가", "iceDmgBoost": "얼음 피해 증가",
"imaginaryDmgBoost": "환상 피해 증가", "imaginaryDmgBoost": "환상 피해 증가",
"physicalDmgBoost": "물리 피해 증가", "physicalDmgBoost": "물리 피해 증가",
"quantumDmgBoost": "양자 피해 증가", "quantumDmgBoost": "양자 피해 증가",
"thunderDmgBoost": "번개 피해 증가", "thunderDmgBoost": "번개 피해 증가",
"windDmgBoost": "바람 피해 증가", "windDmgBoost": "바람 피해 증가",
"pursued": "추가 피해", "pursued": "추가 피해",
"true damage": "진실한 피해", "true damage": "진실한 피해",
"elationdamage": "환락 피해", "elationdamage": "환락 피해",
"follow-up": "후속 피해", "follow-up": "후속 피해",
"elemental damage": "파괴 및 초파괴 피해", "elemental damage": "파괴 및 초파괴 피해",
"dot": "시간 경과 피해", "dot": "시간 경과 피해",
"qte": "QTE 스킬", "qte": "QTE 스킬",
"level": "레벨", "level": "레벨",
"relics": "유물", "relics": "유물",
"eidolons": "에이돌론", "eidolons": "에이돌론",
"lightcones": "라이트콘", "lightcones": "라이트콘",
"loadData": "데이터 불러오기", "loadData": "데이터 불러오기",
"exportData": "데이터 내보내기", "exportData": "데이터 내보내기",
"connectSetting": "연결 설정", "connectSetting": "연결 설정",
"connected": "연결됨", "connected": "연결됨",
"unconnected": "연결 안됨", "unconnected": "연결 안됨",
"psConnection": "PS 연결", "psConnection": "PS 연결",
"connectionType": "연결 타입", "connectionType": "연결 타입",
"status": "상태", "status": "상태",
"connectPs": "PS 연결", "connectPs": "PS 연결",
"disconnect": "연결 끊기", "disconnect": "연결 끊기",
"other": "기타", "other": "기타",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "데이터베이스", "database": "데이터베이스",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "몬스터 설정", "monsterSetting": "몬스터 설정",
"serverUrl": "서버 URL", "serverUrl": "서버 URL",
"privateType": "프라이빗 타입", "privateType": "프라이빗 타입",
"local": "로컬", "local": "로컬",
"server": "서버", "server": "서버",
"username": "사용자명", "username": "사용자명",
"password": "비밀번호", "password": "비밀번호",
"placeholderServerUrl": "서버 URL 입력", "placeholderServerUrl": "서버 URL 입력",
"placeholderUsername": "사용자명 입력", "placeholderUsername": "사용자명 입력",
"placeholderPassword": "비밀번호 입력", "placeholderPassword": "비밀번호 입력",
"connectedSuccess": "PS에 성공적으로 연결됨", "connectedSuccess": "PS에 성공적으로 연결됨",
"connectedFailed": "PS 연결 실패", "connectedFailed": "PS 연결 실패",
"syncSuccess": "PS에 데이터 동기화 성공", "syncSuccess": "PS에 데이터 동기화 성공",
"syncFailed": "PS에 데이터 동기화 실패", "syncFailed": "PS에 데이터 동기화 실패",
"sync": "동기화", "sync": "동기화",
"importSetting": "설정 가져오기", "importSetting": "설정 가져오기",
"profile": "프로필", "profile": "프로필",
"default": "기본", "default": "기본",
"copyProfiles": "프로필 복사", "copyProfiles": "프로필 복사",
"addNewProfile": "새 프로필 추가", "addNewProfile": "새 프로필 추가",
"createNewProfile": "새 프로필 생성", "createNewProfile": "새 프로필 생성",
"editProfile": "프로필 편집", "editProfile": "프로필 편집",
"placeholderProfileName": "프로필 이름 입력", "placeholderProfileName": "프로필 이름 입력",
"profileName": "프로필 이름", "profileName": "프로필 이름",
"create": "생성", "create": "생성",
"update": "업데이트", "update": "업데이트",
"characterInformation": "캐릭터 정보", "characterInformation": "캐릭터 정보",
"skills": "스킬", "skills": "스킬",
"showcaseCard": "쇼케이스 카드", "showcaseCard": "쇼케이스 카드",
"comingSoon": "곧 출시", "comingSoon": "곧 출시",
"characterName": "캐릭터 이름", "characterName": "캐릭터 이름",
"placeholderCharacter": "캐릭터 이름 입력", "placeholderCharacter": "캐릭터 이름 입력",
"characterSettings": "캐릭터 설정", "characterSettings": "캐릭터 설정",
"levelConfiguration": "레벨 구성", "levelConfiguration": "레벨 구성",
"characterLevel": "캐릭터 레벨", "characterLevel": "캐릭터 레벨",
"max": "최대", "max": "최대",
"ultimateEnergy": "궁극 에너지", "ultimateEnergy": "궁극 에너지",
"currentEnergy": "현재 에너지", "currentEnergy": "현재 에너지",
"setTo50": "50%로 설정", "setTo50": "50%로 설정",
"battleConfiguration": "전투 구성", "battleConfiguration": "전투 구성",
"useTechnique": "비기 사용", "useTechnique": "비기 사용",
"techniqueNote": "전투 전 비기 효과 활성화", "techniqueNote": "전투 전 비기 효과 활성화",
"enhancement": "강화", "enhancement": "강화",
"enhancementLevel": "강화 레벨", "enhancementLevel": "강화 레벨",
"origin": "출처", "origin": "출처",
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제", "enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
"lightconeEquipment": "라이트콘 장비", "lightconeEquipment": "라이트콘 장비",
"lightconeSettings": "라이트콘 설정", "lightconeSettings": "라이트콘 설정",
"placeholderLevel": "레벨 입력", "placeholderLevel": "레벨 입력",
"superimpositionRank": "중첩 등급", "superimpositionRank": "중첩 등급",
"ranksNote": "등급이 높을수록 효과가 강함", "ranksNote": "등급이 높을수록 효과가 강함",
"changeLightcone": "라이트콘 변경", "changeLightcone": "라이트콘 변경",
"removeLightcone": "라이트콘 제거", "removeLightcone": "라이트콘 제거",
"equipLightcone": "라이트콘 장착", "equipLightcone": "라이트콘 장착",
"noLightconeEquipped": "라이트콘 미장착", "noLightconeEquipped": "라이트콘 미장착",
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요", "equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
"filter": "필터", "filter": "필터",
"selectedCharacters": "선택된 캐릭터", "selectedCharacters": "선택된 캐릭터",
"selectedProfiles": "선택된 프로필", "selectedProfiles": "선택된 프로필",
"clearAll": "전체 지우기", "clearAll": "전체 지우기",
"selectAll": "전체 선택", "selectAll": "전체 선택",
"copy": "복사", "copy": "복사",
"copied": "복사됨", "copied": "복사됨",
"noAvatarSelected": "캐릭터가 선택되지 않음", "noAvatarSelected": "캐릭터가 선택되지 않음",
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음", "noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요", "pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
"pleaseEnterUid": "UID를 입력해주세요", "pleaseEnterUid": "UID를 입력해주세요",
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패", "failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요", "pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
"noDataToImport": "가져올 데이터가 없습니다", "noDataToImport": "가져올 데이터가 없습니다",
"pleaseSelectAFile": "파일을 선택하세요", "pleaseSelectAFile": "파일을 선택하세요",
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다", "fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공", "importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공", "importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
"importDatabaseSuccess": "데이터베이스 가져오기 성공", "importDatabaseSuccess": "데이터베이스 가져오기 성공",
"getData": "데이터 가져오기", "getData": "데이터 가져오기",
"import": "가져오기", "import": "가져오기",
"freeSRImport": "FreeSR 가져오기", "freeSRImport": "FreeSR 가져오기",
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원", "onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
"pickAFile": "파일 선택", "pickAFile": "파일 선택",
"lightConeSetting": "라이트콘 설정", "lightConeSetting": "라이트콘 설정",
"relicMaker": "유물 제작", "relicMaker": "유물 제작",
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요", "pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
"relicSavedSuccessfully": "유물 저장 성공", "relicSavedSuccessfully": "유물 저장 성공",
"mainSettings": "메인 설정", "mainSettings": "메인 설정",
"mainStat": "주 속성", "mainStat": "주 속성",
"set": "세트", "set": "세트",
"pleaseSelectASet": "세트 하나를 선택해주세요", "pleaseSelectASet": "세트 하나를 선택해주세요",
"effectBonus": "효과 보너스", "effectBonus": "효과 보너스",
"totalRoll": "총 횟수", "totalRoll": "총 횟수",
"randomizeStats": "속성 랜덤화", "randomizeStats": "속성 랜덤화",
"randomizeRolls": "횟수 랜덤화", "randomizeRolls": "횟수 랜덤화",
"selectASubStat": "부 속성 선택", "selectASubStat": "부 속성 선택",
"selectASet": "세트 선택", "selectASet": "세트 선택",
"selectAMainStat": "주 속성 선택", "selectAMainStat": "주 속성 선택",
"save": "저장", "save": "저장",
"reset": "초기화", "reset": "초기화",
"roll": "굴리기", "roll": "굴리기",
"step": "단계", "step": "단계",
"memoryOfChaos": "망각의 정원", "memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기", "pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영", "apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적", "customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스", "simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수", "floor": "층수",
"side": "전반/후반", "side": "전반/후반",
"wave": "웨이브", "wave": "웨이브",
"stage": "스테이지", "stage": "스테이지",
"useCycleCount": "사이클 수 사용?", "useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?", "useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적", "firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적", "secondHalfEnemies": "후반 적",
"firstNodeEnemies": "노드 1 적", "firstNodeEnemies": "노드 1 적",
"secondNodeEnemies": "노드 2 적", "secondNodeEnemies": "노드 2 적",
"thirdNodeEnemies": "노드 3 적", "thirdNodeEnemies": "노드 3 적",
"firstNode": "노드 1", "firstNode": "노드 1",
"secondNode": "노드 2", "secondNode": "노드 2",
"thirdNode": "노드 3", "thirdNode": "노드 3",
"turbulenceBuff": "난류 버프", "turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음", "noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음", "noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반", "upper": "상반",
"lower": "하반", "lower": "하반",
"upperToLower": "상반 -> 하반", "upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반", "lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택", "selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택", "selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택", "selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택", "selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택", "selectEvent": "이벤트 선택",
"selectFloor": "층 선택", "selectFloor": "층 선택",
"selectSide": "전반/후반 선택", "selectSide": "전반/후반 선택",
"selectBuff": "버프 선택", "selectBuff": "버프 선택",
"selectStage": "스테이지 선택", "selectStage": "스테이지 선택",
"previous": "이전", "previous": "이전",
"next": "다음", "next": "다음",
"noMonstersFound": "적을 찾을 수 없음", "noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가", "addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...", "searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음", "noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...", "searchMonster": "적 검색...",
"changeRelic": "유물 변경", "changeRelic": "유물 변경",
"deleteRelic": "유물 삭제", "deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?", "deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정", "setEffects": "효과 설정",
"details": "상세", "details": "상세",
"normal": "기본 공격", "normal": "기본 공격",
"bpskill": "스킬", "bpskill": "스킬",
"maze": "전술", "assist": "지원 스킬",
"ultra": "필살기", "maze": "전술",
"servantskill": "메모스프라이트 스킬", "ultra": "필살기",
"severaltalent": "메모스프라이트 재능", "servantskill": "메모스프라이트 스킬",
"singleattack": "단일 공격", "severaltalent": "메모스프라이트 재능",
"enhance": "강화", "singleattack": "단일 공격",
"summon": "소환", "enhance": "강화",
"blast": "광역 공격", "summon": "소환",
"restore": "회복", "blast": "광역 공격",
"support": "지원", "restore": "회복",
"aoeattack": "광역 공격", "support": "지원",
"mazeattack": "미궁 비기", "aoeattack": "광역 공격",
"impair": "약화", "mazeattack": "미궁 비기",
"bounce": "튕김", "impair": "약화",
"active": "활성", "bounce": "튕김",
"inactive": "비활성", "active": "활성",
"defence": "방어", "inactive": "비활성",
"maxAll": "모두 최대치", "defence": "방어",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.", "maxAll": "모두 최대치",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.", "maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"noRelicEquipped": "성유물이 장착되지 않음", "maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
"anomalyArbitration": "이상 중재", "noRelicEquipped": "성유물이 장착되지 않음",
"normalMode": "일반 모드", "anomalyArbitration": "이상 중재",
"hardMode": "어려움 모드", "normalMode": "일반 모드",
"selectPEAKEvent": "PEAK 이벤트 선택", "hardMode": "어려움 모드",
"mode": "모드", "selectPEAKEvent": "PEAK 이벤트 선택",
"selectMode": "모드를 선택", "mode": "모드",
"rollBack": "이전 상태로 되돌리기", "selectMode": "모드를 선택",
"upRoll": "부옵션 추가", "rollBack": "이전 상태로 되돌리기",
"downRoll": "부옵션 감소", "upRoll": "부옵션 추가",
"actions": "동작", "downRoll": "부옵션 감소",
"avatars": "아바타", "actions": "동작",
"quickView": "빠른 조회", "avatars": "아바타",
"extraSetting": "추가 설정", "quickView": "빠른 조회",
"disableCensorship": "검열 비활성화", "extraSetting": "추가 설정",
"hideUI": "UI 숨기기", "disableCensorship": "검열 비활성화",
"theoryCraftMode": "Theory Craft 모드", "hideUI": "UI 숨기기",
"cycleCount": "사이클 수", "theoryCraftMode": "Theory Craft 모드",
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요", "cycleCount": "사이클 수",
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다", "pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
"theoryCraft": "Theory Craft", "subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
"multipathCharacter": "다중 운명 캐릭터", "theoryCraft": "Theory Craft",
"mainPath": "개척자 운명", "multipathCharacter": "다중 운명 캐릭터",
"march7Path": "삼월칠일 운명", "mainPath": "개척자 운명",
"challenge": "도전", "march7Path": "삼월칠일 운명",
"skipNode": "노드 건너뛰기", "challenge": "도전",
"disableSkip": "건너뛰기 비활성화", "skipNode": "노드 건너뛰기",
"skipNode1": "노드 1 건너뛰기", "disableSkip": "건너뛰기 비활성화",
"skipNode2": "노드 2 건너뛰기", "skipNode1": "노드 1 건너뛰기",
"extraFeatures": "추가 기능", "skipNode2": "노드 2 건너뛰기",
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.", "extraFeatures": "추가 기능",
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.", "detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.", "detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.", "detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.", "detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.", "detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
"trailblazer": "개척자", "detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
"listExtraEffect": "추가 효과 목록", "trailblazer": "개척자",
"extra": "추가", "listExtraEffect": "추가 효과 목록",
"customLineup": "커스텀 편성" "extra": "추가",
} "customLineup": "커스텀 편성",
} "def": "방어력",
"language": "언어"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Ferramentas Firefly por Firefly Shelter" "description": "Ferramentas Firefly por Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Tipo de Habilidade", "skillType": "Tipo de Habilidade",
"skillName": "Nome da Habilidade", "skillName": "Nome da Habilidade",
"character": "Personagem", "character": "Personagem",
"id": "Id", "id": "Id",
"path": "Caminho", "path": "Caminho",
"rarity": "Raridade", "rarity": "Raridade",
"element": "Elemento", "element": "Elemento",
"technique": "Técnica", "technique": "Técnica",
"talent": "Talento", "talent": "Talento",
"basic": "Ataque Básico", "basic": "Ataque Básico",
"skill": "Perícia", "skill": "Perícia",
"ultimate": "Perícia Suprema", "ultimate": "Perícia Suprema",
"servant": "Servo", "servant": "Servo",
"damage": "Dano", "damage": "Dano",
"type": "Tipo", "type": "Tipo",
"warrior": "A Destruição", "warrior": "A Destruição",
"knight": "A Preservação", "knight": "A Preservação",
"mage": "A Erudição", "mage": "A Erudição",
"priest": "A Abundância", "priest": "A Abundância",
"rogue": "A Caça", "rogue": "A Caça",
"shaman": "A Harmonia", "shaman": "A Harmonia",
"warlock": "A Inexistência", "warlock": "A Inexistência",
"memory": "A Lembrança", "memory": "A Lembrança",
"elation": "A Alegria", "elation": "A Alegria",
"fire": "Fogo", "fire": "Fogo",
"ice": "Gelo", "ice": "Gelo",
"imaginary": "Imaginário", "imaginary": "Imaginário",
"physical": "Físico", "physical": "Físico",
"quantum": "Quântico", "quantum": "Quântico",
"thunder": "Raio", "thunder": "Raio",
"wind": "Vento", "wind": "Vento",
"hp": "PV", "hp": "PV",
"atk": "ATQ", "atk": "ATQ",
"speed": "VEL", "speed": "VEL",
"critRate": "Taxa CRIT", "critRate": "Taxa CRIT",
"critDmg": "Dano CRIT", "critDmg": "Dano CRIT",
"breakEffect": "Efeito de Quebra", "breakEffect": "Efeito de Quebra",
"effectRes": "RES a Efeito", "effectRes": "RES a Efeito",
"energyRegenerationRate": "Taxa de Regeneração de Energia", "energyRegenerationRate": "Taxa de Regeneração de Energia",
"effectHitRate": "Taxa de Acerto de Efeito", "effectHitRate": "Taxa de Acerto de Efeito",
"outgoingHealingBoost": "Bônus de Cura", "outgoingHealingBoost": "Bônus de Cura",
"fireDmgBoost": "Bônus de Dano de Fogo", "fireDmgBoost": "Bônus de Dano de Fogo",
"iceDmgBoost": "Bônus de Dano de Gelo", "iceDmgBoost": "Bônus de Dano de Gelo",
"imaginaryDmgBoost": "Bônus de Dano Imaginário", "imaginaryDmgBoost": "Bônus de Dano Imaginário",
"physicalDmgBoost": "Bônus de Dano Físico", "physicalDmgBoost": "Bônus de Dano Físico",
"quantumDmgBoost": "Bônus de Dano Quântico", "quantumDmgBoost": "Bônus de Dano Quântico",
"thunderDmgBoost": "Bônus de Dano de Raio", "thunderDmgBoost": "Bônus de Dano de Raio",
"windDmgBoost": "Bônus de Dano de Vento", "windDmgBoost": "Bônus de Dano de Vento",
"pursued": "Dano Adicional", "pursued": "Dano Adicional",
"true damage": "Dano Verdadeiro", "true damage": "Dano Verdadeiro",
"elationdamage": "Dano de Alegria", "elationdamage": "Dano de Alegria",
"follow-up": "Dano de Ataque Extra", "follow-up": "Dano de Ataque Extra",
"elemental damage": "Dano de Quebra e Superquebra", "elemental damage": "Dano de Quebra e Superquebra",
"dot": "Dano Contínuo", "dot": "Dano Contínuo",
"qte": "Habilidade QTE", "qte": "Habilidade QTE",
"level": "Nível", "level": "Nível",
"relics": "Relíquias", "relics": "Relíquias",
"eidolons": "Eidolons", "eidolons": "Eidolons",
"lightcones": "Cones de Luz", "lightcones": "Cones de Luz",
"loadData": "Carregar dados", "loadData": "Carregar dados",
"exportData": "Exportar dados", "exportData": "Exportar dados",
"connectSetting": "Configuração de Conexão", "connectSetting": "Configuração de Conexão",
"connected": "Conectado", "connected": "Conectado",
"unconnected": "Desconectado", "unconnected": "Desconectado",
"psConnection": "Conexão PS", "psConnection": "Conexão PS",
"connectionType": "Tipo de Conexão", "connectionType": "Tipo de Conexão",
"status": "Status", "status": "Status",
"connectPs": "Conectar PS", "connectPs": "Conectar PS",
"disconnect": "Desconectar", "disconnect": "Desconectar",
"other": "Outro", "other": "Outro",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Banco de dados", "database": "Banco de dados",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Configuração de Monstros", "monsterSetting": "Configuração de Monstros",
"serverUrl": "URL do Servidor", "serverUrl": "URL do Servidor",
"privateType": "Tipo Privado", "privateType": "Tipo Privado",
"local": "Local", "local": "Local",
"server": "Servidor", "server": "Servidor",
"username": "Usuário", "username": "Usuário",
"password": "Senha", "password": "Senha",
"placeholderServerUrl": "Digite o URL do servidor", "placeholderServerUrl": "Digite o URL do servidor",
"placeholderUsername": "Digite o usuário", "placeholderUsername": "Digite o usuário",
"placeholderPassword": "Digite a senha", "placeholderPassword": "Digite a senha",
"connectedSuccess": "Conectado ao PS com sucesso", "connectedSuccess": "Conectado ao PS com sucesso",
"connectedFailed": "Falha ao conectar ao PS", "connectedFailed": "Falha ao conectar ao PS",
"syncSuccess": "Dados sincronizados com o PS com sucesso", "syncSuccess": "Dados sincronizados com o PS com sucesso",
"syncFailed": "Falha ao sincronizar dados com o PS", "syncFailed": "Falha ao sincronizar dados com o PS",
"sync": "Sincronizar", "sync": "Sincronizar",
"importSetting": "Configuração de Importação", "importSetting": "Configuração de Importação",
"profile": "Perfil", "profile": "Perfil",
"default": "Padrão", "default": "Padrão",
"copyProfiles": "Copiar perfis", "copyProfiles": "Copiar perfis",
"addNewProfile": "Adicionar novo perfil", "addNewProfile": "Adicionar novo perfil",
"createNewProfile": "Criar novo perfil", "createNewProfile": "Criar novo perfil",
"editProfile": "Editar perfil", "editProfile": "Editar perfil",
"placeholderProfileName": "Digite o nome do perfil", "placeholderProfileName": "Digite o nome do perfil",
"profileName": "Nome do perfil", "profileName": "Nome do perfil",
"create": "Criar", "create": "Criar",
"update": "Atualizar", "update": "Atualizar",
"characterInformation": "Informação do Personagem", "characterInformation": "Informação do Personagem",
"skills": "Habilidades", "skills": "Habilidades",
"showcaseCard": "Cartão de Exibição", "showcaseCard": "Cartão de Exibição",
"comingSoon": "Em breve", "comingSoon": "Em breve",
"characterName": "Nome do personagem", "characterName": "Nome do personagem",
"placeholderCharacter": "Digite o nome do personagem", "placeholderCharacter": "Digite o nome do personagem",
"characterSettings": "Configurações de Personagem", "characterSettings": "Configurações de Personagem",
"levelConfiguration": "Configuração de Nível", "levelConfiguration": "Configuração de Nível",
"characterLevel": "Nível do Personagem", "characterLevel": "Nível do Personagem",
"max": "MÁX", "max": "MÁX",
"ultimateEnergy": "Energia da Perícia Suprema", "ultimateEnergy": "Energia da Perícia Suprema",
"currentEnergy": "Energia Atual", "currentEnergy": "Energia Atual",
"setTo50": "Definir para 50%", "setTo50": "Definir para 50%",
"battleConfiguration": "Configuração de Batalha", "battleConfiguration": "Configuração de Batalha",
"useTechnique": "Usar Técnica", "useTechnique": "Usar Técnica",
"techniqueNote": "Ativar efeitos de técnica pré-batalha", "techniqueNote": "Ativar efeitos de técnica pré-batalha",
"enhancement": "Aprimoramento", "enhancement": "Aprimoramento",
"enhancementLevel": "Nível de Aprimoramento", "enhancementLevel": "Nível de Aprimoramento",
"origin": "Origem", "origin": "Origem",
"enhancedNote": "Níveis altos desbloqueiam habilidades", "enhancedNote": "Níveis altos desbloqueiam habilidades",
"lightconeEquipment": "Equipamento de Cone de Luz", "lightconeEquipment": "Equipamento de Cone de Luz",
"lightconeSettings": "Configurações de Cone de Luz", "lightconeSettings": "Configurações de Cone de Luz",
"placeholderLevel": "Digite o nível", "placeholderLevel": "Digite o nível",
"superimpositionRank": "Rank de Sobreposição", "superimpositionRank": "Rank de Sobreposição",
"ranksNote": "Ranks maiores dão efeitos mais fortes", "ranksNote": "Ranks maiores dão efeitos mais fortes",
"changeLightcone": "Mudar Cone de Luz", "changeLightcone": "Mudar Cone de Luz",
"removeLightcone": "Remover Cone de Luz", "removeLightcone": "Remover Cone de Luz",
"equipLightcone": "Equipar Cone de Luz", "equipLightcone": "Equipar Cone de Luz",
"noLightconeEquipped": "Sem Cone de Luz", "noLightconeEquipped": "Sem Cone de Luz",
"equipLightconeNote": "Equipe um cone de luz para melhorar o personagem", "equipLightconeNote": "Equipe um cone de luz para melhorar o personagem",
"filter": "Filtro", "filter": "Filtro",
"selectedCharacters": "Personagens Selecionados", "selectedCharacters": "Personagens Selecionados",
"selectedProfiles": "Perfis Selecionados", "selectedProfiles": "Perfis Selecionados",
"clearAll": "Limpar Tudo", "clearAll": "Limpar Tudo",
"selectAll": "Selecionar Tudo", "selectAll": "Selecionar Tudo",
"copy": "Copiar", "copy": "Copiar",
"copied": "Copiado", "copied": "Copiado",
"noAvatarSelected": "Nenhum personagem selecionado", "noAvatarSelected": "Nenhum personagem selecionado",
"noAvatarToCopySelected": "Nenhum personagem para copiar", "noAvatarToCopySelected": "Nenhum personagem para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil", "pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil",
"pleaseEnterUid": "Por favor, insira o UID", "pleaseEnterUid": "Por favor, insira o UID",
"failedToFetchEnkaData": "Falha ao obter dados Enka", "failedToFetchEnkaData": "Falha ao obter dados Enka",
"pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem", "pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem",
"noDataToImport": "Sem dados para importar", "noDataToImport": "Sem dados para importar",
"pleaseSelectAFile": "Por favor, selecione um arquivo", "pleaseSelectAFile": "Por favor, selecione um arquivo",
"fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido", "fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido",
"importEnkaDataSuccess": "Dados Enka importados com sucesso", "importEnkaDataSuccess": "Dados Enka importados com sucesso",
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso", "importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
"importDatabaseSuccess": "Banco de dados importado com sucesso", "importDatabaseSuccess": "Banco de dados importado com sucesso",
"getData": "Obter Dados", "getData": "Obter Dados",
"import": "Importar", "import": "Importar",
"freeSRImport": "Importação FreeSR", "freeSRImport": "Importação FreeSR",
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR", "onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
"pickAFile": "Escolher um arquivo", "pickAFile": "Escolher um arquivo",
"lightConeSetting": "Configuração de Cone de Luz", "lightConeSetting": "Configuração de Cone de Luz",
"relicMaker": "Criador de Relíquias", "relicMaker": "Criador de Relíquias",
"pleaseSelectAllOptions": "Por favor selecione todas as opções", "pleaseSelectAllOptions": "Por favor selecione todas as opções",
"relicSavedSuccessfully": "Relíquia salva com sucesso", "relicSavedSuccessfully": "Relíquia salva com sucesso",
"mainSettings": "Configurações Principais", "mainSettings": "Configurações Principais",
"mainStat": "Atributo Principal", "mainStat": "Atributo Principal",
"set": "Conjunto", "set": "Conjunto",
"pleaseSelectASet": "Selecione um conjunto", "pleaseSelectASet": "Selecione um conjunto",
"effectBonus": "Bônus de Efeito", "effectBonus": "Bônus de Efeito",
"totalRoll": "Total de Melhorias", "totalRoll": "Total de Melhorias",
"randomizeStats": "Atributos Aleatórios", "randomizeStats": "Atributos Aleatórios",
"randomizeRolls": "Melhorias Aleatórias", "randomizeRolls": "Melhorias Aleatórias",
"selectASubStat": "Selecionar sub-atributo", "selectASubStat": "Selecionar sub-atributo",
"selectASet": "Selecionar conjunto", "selectASet": "Selecionar conjunto",
"selectAMainStat": "Selecionar atributo principal", "selectAMainStat": "Selecionar atributo principal",
"save": "Salvar", "save": "Salvar",
"reset": "Redefinir", "reset": "Redefinir",
"roll": "Melhoria", "roll": "Melhoria",
"step": "Passo", "step": "Passo",
"memoryOfChaos": "Memória do Caos", "memoryOfChaos": "Memória do Caos",
"pureFiction": "Pura Ficção", "pureFiction": "Pura Ficção",
"apocalypticShadow": "Sombra Apocalíptica", "apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Inimigo Personalizado", "customEnemy": "Inimigo Personalizado",
"simulatedUniverse": "Universo Simulado", "simulatedUniverse": "Universo Simulado",
"floor": "Andar", "floor": "Andar",
"side": "Lado", "side": "Lado",
"wave": "Onda", "wave": "Onda",
"stage": "Fase", "stage": "Fase",
"useCycleCount": "Usar contagem de ciclos?", "useCycleCount": "Usar contagem de ciclos?",
"useTurbulenceBuff": "Usar buff de turbulência?", "useTurbulenceBuff": "Usar buff de turbulência?",
"firstHalfEnemies": "Inimigos da primeira metade", "firstHalfEnemies": "Inimigos da primeira metade",
"secondHalfEnemies": "Inimigos da segunda metade", "secondHalfEnemies": "Inimigos da segunda metade",
"firstNodeEnemies": "Inimigos do nodo 1", "firstNodeEnemies": "Inimigos do nodo 1",
"secondNodeEnemies": "Inimigos do nodo 2", "secondNodeEnemies": "Inimigos do nodo 2",
"thirdNodeEnemies": "Inimigos do nodo 3", "thirdNodeEnemies": "Inimigos do nodo 3",
"firstNode": "Nodo 1", "firstNode": "Nodo 1",
"secondNode": "Nodo 2", "secondNode": "Nodo 2",
"thirdNode": "Nodo 3", "thirdNode": "Nodo 3",
"listEnemies": "Lista de inimigos", "listEnemies": "Lista de inimigos",
"turbulenceBuff": "Buff de Turbulência", "turbulenceBuff": "Buff de Turbulência",
"noEventSelected": "Nenhum evento selecionado", "noEventSelected": "Nenhum evento selecionado",
"noTurbulenceBuff": "Sem Buff de Turbulência", "noTurbulenceBuff": "Sem Buff de Turbulência",
"upper": "Superior", "upper": "Superior",
"lower": "Inferior", "lower": "Inferior",
"upperToLower": "Superior -> Inferior", "upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior", "lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Selecionar Evento MOC", "selectMOCEvent": "Selecionar Evento MOC",
"selectPFEvent": "Selecionar Evento PF", "selectPFEvent": "Selecionar Evento PF",
"selectASEvent": "Selecionar Evento AS", "selectASEvent": "Selecionar Evento AS",
"selectCEEvent": "Selecionar Evento CE", "selectCEEvent": "Selecionar Evento CE",
"selectEvent": "Selecionar Evento", "selectEvent": "Selecionar Evento",
"selectFloor": "Selecionar Andar", "selectFloor": "Selecionar Andar",
"selectSide": "Selecionar Lado", "selectSide": "Selecionar Lado",
"selectBuff": "Selecionar Buff", "selectBuff": "Selecionar Buff",
"selectStage": "Selecionar Fase", "selectStage": "Selecionar Fase",
"previous": "Anterior", "previous": "Anterior",
"next": "Próximo", "next": "Próximo",
"noMonstersFound": "Nenhum monstro encontrado", "noMonstersFound": "Nenhum monstro encontrado",
"addNewWave": "Adicionar Nova Onda", "addNewWave": "Adicionar Nova Onda",
"searchStage": "Buscar fase...", "searchStage": "Buscar fase...",
"noStageFound": "Fase não encontrada", "noStageFound": "Fase não encontrada",
"searchMonster": "Buscar monstro...", "searchMonster": "Buscar monstro...",
"changeRelic": "Mudar relíquia", "changeRelic": "Mudar relíquia",
"deleteRelic": "Deletar relíquia", "deleteRelic": "Deletar relíquia",
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?", "deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
"setEffects": "Definir Efeitos", "setEffects": "Definir Efeitos",
"details": "Detalhes", "details": "Detalhes",
"normal": "Ataque Básico", "normal": "Ataque Básico",
"bpskill": "Perícia", "bpskill": "Perícia",
"maze": "Técnica", "maze": "Técnica",
"ultra": "Perícia Suprema", "ultra": "Perícia Suprema",
"servantskill": "Habilidade do Memoespirito", "assist": "Habilidade de Assistência",
"severaltalent": "Talento do Memoespirito", "servantskill": "Habilidade do Memoespirito",
"singleattack": "Ataque Único", "severaltalent": "Talento do Memoespirito",
"enhance": "Aprimorar", "singleattack": "Ataque Único",
"summon": "Invocar", "enhance": "Aprimorar",
"mazeattack": "Ataque de Técnica", "summon": "Invocar",
"blast": "Explosão", "mazeattack": "Ataque de Técnica",
"restore": "Restaurar", "blast": "Explosão",
"support": "Suporte", "restore": "Restaurar",
"aoeattack": "Ataque em Área", "support": "Suporte",
"impair": "Enfraquecer", "aoeattack": "Ataque em Área",
"bounce": "Rebote", "impair": "Enfraquecer",
"active": "Ativo", "bounce": "Rebote",
"defence": "Defesa", "active": "Ativo",
"inactive": "Inativo", "defence": "Defesa",
"maxAll": "Maximizar Tudo", "inactive": "Inativo",
"maxAllSuccess": "Habilidades maximizadas com sucesso.", "maxAll": "Maximizar Tudo",
"maxAllFailed": "Falha ao maximizar habilidades.", "maxAllSuccess": "Habilidades maximizadas com sucesso.",
"noRelicEquipped": "Sem relíquia equipada", "maxAllFailed": "Falha ao maximizar habilidades.",
"anomalyArbitration": "Arbitragem de Anomalia", "noRelicEquipped": "Sem relíquia equipada",
"normalMode": "Modo Normal", "anomalyArbitration": "Arbitragem de Anomalia",
"hardMode": "Modo Difícil", "normalMode": "Modo Normal",
"selectPEAKEvent": "Selecionar Evento PEAK", "hardMode": "Modo Difícil",
"mode": "Modo", "selectPEAKEvent": "Selecionar Evento PEAK",
"selectMode": "Selecionar um modo", "mode": "Modo",
"rollBack": "Reverter", "selectMode": "Selecionar um modo",
"upRoll": "Aumentar Melhoria", "rollBack": "Reverter",
"downRoll": "Diminuir Melhoria", "upRoll": "Aumentar Melhoria",
"actions": "Ações", "downRoll": "Diminuir Melhoria",
"avatars": "Avatares", "actions": "Ações",
"quickView": "Visualização Rápida", "avatars": "Avatares",
"extraSetting": "Configurações Extras", "quickView": "Visualização Rápida",
"disableCensorship": "Desativar Censura", "extraSetting": "Configurações Extras",
"hideUI": "Ocultar UI", "disableCensorship": "Desativar Censura",
"theoryCraftMode": "Modo Theorycraft", "hideUI": "Ocultar UI",
"cycleCount": "Contagem de Ciclos", "theoryCraftMode": "Modo Theorycraft",
"pleaseSelectAllSubStats": "Selecione todos os sub-atributos", "cycleCount": "Contagem de Ciclos",
"subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero", "pleaseSelectAllSubStats": "Selecione todos os sub-atributos",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero",
"multipathCharacter": "Personagem Multicaminho", "theoryCraft": "Theorycraft",
"mainPath": "Caminho Principal", "multipathCharacter": "Personagem Multicaminho",
"march7Path": "Caminho de 7 de Março", "mainPath": "Caminho Principal",
"challenge": "Desafio", "march7Path": "Caminho de 7 de Março",
"skipNode": "Pular Nó", "challenge": "Desafio",
"disableSkip": "Desativar pulo", "skipNode": "Pular Nó",
"skipNode1": "Pular nó 1", "disableSkip": "Desativar pulo",
"skipNode2": "Pular nó 2", "skipNode1": "Pular nó 1",
"extraFeatures": "Recursos Extras", "skipNode2": "Pular nó 2",
"detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.", "extraFeatures": "Recursos Extras",
"detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.", "detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.",
"detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.", "detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.",
"detailHiddenUi": "Irá esconder a interface do jogo.", "detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.",
"detailDisableCensorship": "Desativa a censura no jogo.", "detailHiddenUi": "Irá esconder a interface do jogo.",
"detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.", "detailDisableCensorship": "Desativa a censura no jogo.",
"trailblazer": "Desbravador", "detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.",
"listExtraEffect": "Lista de Efeitos Extras", "trailblazer": "Desbravador",
"extra": "Extra", "listExtraEffect": "Lista de Efeitos Extras",
"customLineup": "Formação Personalizada" "extra": "Extra",
} "customLineup": "Formação Personalizada",
} "def": "DEF",
"language": "Idioma"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Инструменты Firefly от Firefly Shelter" "description": "Инструменты Firefly от Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Тип навыка", "skillType": "Тип навыка",
"skillName": "Название навыка", "skillName": "Название навыка",
"character": "Персонаж", "character": "Персонаж",
"id": "ID", "id": "ID",
"path": "Путь", "path": "Путь",
"rarity": "Редкость", "rarity": "Редкость",
"element": "Элемент", "element": "Элемент",
"technique": "Техника", "technique": "Техника",
"talent": "Талант", "talent": "Талант",
"basic": "Базовая атака", "basic": "Базовая атака",
"skill": "Навык", "skill": "Навык",
"ultimate": "Сверхспособность", "ultimate": "Сверхспособность",
"servant": "Слуга", "servant": "Слуга",
"damage": "Урон", "damage": "Урон",
"type": "Тип", "type": "Тип",
"warrior": "Разрушение", "warrior": "Разрушение",
"knight": "Сохранение", "knight": "Сохранение",
"mage": "Эрудиция", "mage": "Эрудиция",
"priest": "Изобилие", "priest": "Изобилие",
"rogue": "Охота", "rogue": "Охота",
"shaman": "Гармония", "shaman": "Гармония",
"warlock": "Небытие", "warlock": "Небытие",
"memory": "Память", "memory": "Память",
"elation": "Радость", "elation": "Радость",
"fire": "Огонь", "fire": "Огонь",
"ice": "Лед", "ice": "Лед",
"imaginary": "Мнимый", "imaginary": "Мнимый",
"physical": "Физический", "physical": "Физический",
"quantum": "Квантовый", "quantum": "Квантовый",
"thunder": "Электрический", "thunder": "Электрический",
"wind": "Ветряной", "wind": "Ветряной",
"hp": "HP", "hp": "HP",
"atk": "Сила атаки", "atk": "Сила атаки",
"speed": "Скорость", "speed": "Скорость",
"critRate": "Крит. шанс", "critRate": "Крит. шанс",
"critDmg": "Крит. урон", "critDmg": "Крит. урон",
"breakEffect": "Эффект пробития", "breakEffect": "Эффект пробития",
"effectRes": "Сопротивление эффектам", "effectRes": "Сопротивление эффектам",
"energyRegenerationRate": "Скорость восст. энергии", "energyRegenerationRate": "Скорость восст. энергии",
"effectHitRate": "Шанс попадания эффектов", "effectHitRate": "Шанс попадания эффектов",
"outgoingHealingBoost": "Бонус исходящего исцеления", "outgoingHealingBoost": "Бонус исходящего исцеления",
"fireDmgBoost": "Бонус огненного урона", "fireDmgBoost": "Бонус огненного урона",
"iceDmgBoost": "Бонус ледяного урона", "iceDmgBoost": "Бонус ледяного урона",
"imaginaryDmgBoost": "Бонус мнимого урона", "imaginaryDmgBoost": "Бонус мнимого урона",
"physicalDmgBoost": "Бонус физ. урона", "physicalDmgBoost": "Бонус физ. урона",
"quantumDmgBoost": "Бонус квантового урона", "quantumDmgBoost": "Бонус квантового урона",
"thunderDmgBoost": "Бонус электр. урона", "thunderDmgBoost": "Бонус электр. урона",
"windDmgBoost": "Бонус ветряного урона", "windDmgBoost": "Бонус ветряного урона",
"pursued": "Дополнительный урон", "pursued": "Дополнительный урон",
"true damage": "Истинный урон", "true damage": "Истинный урон",
"elationdamage": "Урон радости", "elationdamage": "Урон радости",
"follow-up": "Урон бонус-атаки", "follow-up": "Урон бонус-атаки",
"elemental damage": "Урон пробития и суперпробития", "elemental damage": "Урон пробития и суперпробития",
"dot": "Периодический урон", "dot": "Периодический урон",
"qte": "QTE Навык", "qte": "QTE Навык",
"level": "Уровень", "level": "Уровень",
"relics": "Реликвии", "relics": "Реликвии",
"eidolons": "Эйдолоны", "eidolons": "Эйдолоны",
"lightcones": "Световые конусы", "lightcones": "Световые конусы",
"loadData": "Загрузить данные", "loadData": "Загрузить данные",
"exportData": "Экспортировать данные", "exportData": "Экспортировать данные",
"connectSetting": "Настройки подключения", "connectSetting": "Настройки подключения",
"connected": "Подключено", "connected": "Подключено",
"unconnected": "Не подключено", "unconnected": "Не подключено",
"psConnection": "Подключение к PS", "psConnection": "Подключение к PS",
"connectionType": "Тип подключения", "connectionType": "Тип подключения",
"status": "Статус", "status": "Статус",
"connectPs": "Подключить PS", "connectPs": "Подключить PS",
"disconnect": "Отключить", "disconnect": "Отключить",
"other": "Другое", "other": "Другое",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "База данных", "database": "База данных",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Настройки монстров", "monsterSetting": "Настройки монстров",
"serverUrl": "URL сервера", "serverUrl": "URL сервера",
"privateType": "Приватный тип", "privateType": "Приватный тип",
"local": "Локальный", "local": "Локальный",
"server": "Сервер", "server": "Сервер",
"username": "Имя пользователя", "username": "Имя пользователя",
"password": "Пароль", "password": "Пароль",
"placeholderServerUrl": "Введите URL сервера", "placeholderServerUrl": "Введите URL сервера",
"placeholderUsername": "Введите имя пользователя", "placeholderUsername": "Введите имя пользователя",
"placeholderPassword": "Введите пароль", "placeholderPassword": "Введите пароль",
"connectedSuccess": "Успешное подключение к PS", "connectedSuccess": "Успешное подключение к PS",
"connectedFailed": "Ошибка подключения к PS", "connectedFailed": "Ошибка подключения к PS",
"syncSuccess": "Данные успешно синхронизированы с PS", "syncSuccess": "Данные успешно синхронизированы с PS",
"syncFailed": "Ошибка синхронизации данных", "syncFailed": "Ошибка синхронизации данных",
"sync": "Синхронизация", "sync": "Синхронизация",
"importSetting": "Настройки импорта", "importSetting": "Настройки импорта",
"profile": "Профиль", "profile": "Профиль",
"default": "По умолчанию", "default": "По умолчанию",
"copyProfiles": "Копировать профили", "copyProfiles": "Копировать профили",
"addNewProfile": "Добавить новый профиль", "addNewProfile": "Добавить новый профиль",
"createNewProfile": "Создать новый профиль", "createNewProfile": "Создать новый профиль",
"editProfile": "Редактировать профиль", "editProfile": "Редактировать профиль",
"placeholderProfileName": "Введите имя профиля", "placeholderProfileName": "Введите имя профиля",
"profileName": "Имя профиля", "profileName": "Имя профиля",
"create": "Создать", "create": "Создать",
"update": "Обновить", "update": "Обновить",
"characterInformation": "Информация о персонаже", "characterInformation": "Информация о персонаже",
"skills": "Навыки", "skills": "Навыки",
"showcaseCard": "Карточка персонажа", "showcaseCard": "Карточка персонажа",
"comingSoon": "Скоро", "comingSoon": "Скоро",
"characterName": "Имя персонажа", "characterName": "Имя персонажа",
"placeholderCharacter": "Введите имя персонажа", "placeholderCharacter": "Введите имя персонажа",
"characterSettings": "Настройки персонажа", "characterSettings": "Настройки персонажа",
"levelConfiguration": "Конфигурация уровня", "levelConfiguration": "Конфигурация уровня",
"characterLevel": "Уровень персонажа", "characterLevel": "Уровень персонажа",
"max": "МАКС", "max": "МАКС",
"ultimateEnergy": "Энергия сверхспособности", "ultimateEnergy": "Энергия сверхспособности",
"currentEnergy": "Текущая энергия", "currentEnergy": "Текущая энергия",
"setTo50": "Установить на 50%", "setTo50": "Установить на 50%",
"battleConfiguration": "Конфигурация боя", "battleConfiguration": "Конфигурация боя",
"useTechnique": "Использовать технику", "useTechnique": "Использовать технику",
"techniqueNote": "Включить эффекты техники до боя", "techniqueNote": "Включить эффекты техники до боя",
"enhancement": "Усиление", "enhancement": "Усиление",
"enhancementLevel": "Уровень усиления", "enhancementLevel": "Уровень усиления",
"origin": "Оригинал", "origin": "Оригинал",
"enhancedNote": "Высокие уровни открывают новые способности", "enhancedNote": "Высокие уровни открывают новые способности",
"lightconeEquipment": "Снаряжение конуса", "lightconeEquipment": "Снаряжение конуса",
"lightconeSettings": "Настройки светового конуса", "lightconeSettings": "Настройки светового конуса",
"placeholderLevel": "Введите уровень", "placeholderLevel": "Введите уровень",
"superimpositionRank": "Уровень наложения", "superimpositionRank": "Уровень наложения",
"ranksNote": "Высокие ранги дают более сильные эффекты", "ranksNote": "Высокие ранги дают более сильные эффекты",
"changeLightcone": "Изменить световой конус", "changeLightcone": "Изменить световой конус",
"removeLightcone": "Снять световой конус", "removeLightcone": "Снять световой конус",
"equipLightcone": "Экипировать конус", "equipLightcone": "Экипировать конус",
"noLightconeEquipped": "Световой конус не экипирован", "noLightconeEquipped": "Световой конус не экипирован",
"equipLightconeNote": "Наденьте конус для усиления персонажа", "equipLightconeNote": "Наденьте конус для усиления персонажа",
"filter": "Фильтр", "filter": "Фильтр",
"selectedCharacters": "Выбранные персонажи", "selectedCharacters": "Выбранные персонажи",
"selectedProfiles": "Выбранные профили", "selectedProfiles": "Выбранные профили",
"clearAll": "Очистить всё", "clearAll": "Очистить всё",
"selectAll": "Выбрать всё", "selectAll": "Выбрать всё",
"copy": "Копировать", "copy": "Копировать",
"copied": "Скопировано", "copied": "Скопировано",
"noAvatarSelected": "Персонаж не выбран", "noAvatarSelected": "Персонаж не выбран",
"noAvatarToCopySelected": "Нет персонажа для копирования", "noAvatarToCopySelected": "Нет персонажа для копирования",
"pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль", "pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль",
"pleaseEnterUid": "Пожалуйста, введите UID", "pleaseEnterUid": "Пожалуйста, введите UID",
"failedToFetchEnkaData": "Не удалось получить данные Enka", "failedToFetchEnkaData": "Не удалось получить данные Enka",
"pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа", "pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа",
"noDataToImport": "Нет данных для импорта", "noDataToImport": "Нет данных для импорта",
"pleaseSelectAFile": "Пожалуйста, выберите файл", "pleaseSelectAFile": "Пожалуйста, выберите файл",
"fileMustBeAValidJsonFile": "Файл должен быть в формате JSON", "fileMustBeAValidJsonFile": "Файл должен быть в формате JSON",
"importEnkaDataSuccess": "Успешный импорт из Enka", "importEnkaDataSuccess": "Успешный импорт из Enka",
"importFreeSRDataSuccess": "Успешный импорт из FreeSR", "importFreeSRDataSuccess": "Успешный импорт из FreeSR",
"importDatabaseSuccess": "Успешный импорт базы данных", "importDatabaseSuccess": "Успешный импорт базы данных",
"getData": "Получить данные", "getData": "Получить данные",
"import": "Импорт", "import": "Импорт",
"freeSRImport": "Импорт FreeSR", "freeSRImport": "Импорт FreeSR",
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR", "onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
"pickAFile": "Выберите файл", "pickAFile": "Выберите файл",
"lightConeSetting": "Настройки конуса", "lightConeSetting": "Настройки конуса",
"relicMaker": "Создатель реликвий", "relicMaker": "Создатель реликвий",
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры", "pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
"relicSavedSuccessfully": "Реликвия успешно сохранена", "relicSavedSuccessfully": "Реликвия успешно сохранена",
"mainSettings": "Основные настройки", "mainSettings": "Основные настройки",
"mainStat": "Основная характеристика", "mainStat": "Основная характеристика",
"set": "Набор", "set": "Набор",
"pleaseSelectASet": "Выберите набор", "pleaseSelectASet": "Выберите набор",
"effectBonus": "Бонус эффекта", "effectBonus": "Бонус эффекта",
"totalRoll": "Всего улучшений", "totalRoll": "Всего улучшений",
"randomizeStats": "Случайные статы", "randomizeStats": "Случайные статы",
"randomizeRolls": "Случайные улучшения", "randomizeRolls": "Случайные улучшения",
"selectASubStat": "Выберите доп. стат", "selectASubStat": "Выберите доп. стат",
"selectASet": "Выберите набор", "selectASet": "Выберите набор",
"selectAMainStat": "Выберите основной стат", "selectAMainStat": "Выберите основной стат",
"save": "Сохранить", "save": "Сохранить",
"reset": "Сброс", "reset": "Сброс",
"roll": "Улучшение", "roll": "Улучшение",
"step": "Шаг", "step": "Шаг",
"memoryOfChaos": "Зал забвения", "memoryOfChaos": "Зал забвения",
"pureFiction": "Чистый вымысел", "pureFiction": "Чистый вымысел",
"apocalypticShadow": "Апокалиптическая тень", "apocalypticShadow": "Апокалиптическая тень",
"customEnemy": "Свой противник", "customEnemy": "Свой противник",
"simulatedUniverse": "Виртуальная вселенная", "simulatedUniverse": "Виртуальная вселенная",
"floor": "Этаж", "floor": "Этаж",
"side": "Половина", "side": "Половина",
"wave": "Волна", "wave": "Волна",
"stage": "Стадия", "stage": "Стадия",
"useCycleCount": "Использовать счетчик циклов?", "useCycleCount": "Использовать счетчик циклов?",
"useTurbulenceBuff": "Использовать бафф турбулентности?", "useTurbulenceBuff": "Использовать бафф турбулентности?",
"firstHalfEnemies": "Враги первой половины", "firstHalfEnemies": "Враги первой половины",
"secondHalfEnemies": "Враги второй половины", "secondHalfEnemies": "Враги второй половины",
"firstNodeEnemies": "Враги узла 1", "firstNodeEnemies": "Враги узла 1",
"secondNodeEnemies": "Враги узла 2", "secondNodeEnemies": "Враги узла 2",
"thirdNodeEnemies": "Враги узла 3", "thirdNodeEnemies": "Враги узла 3",
"firstNode": "Узел 1", "firstNode": "Узел 1",
"secondNode": "Узел 2", "secondNode": "Узел 2",
"thirdNode": "Узел 3", "thirdNode": "Узел 3",
"listEnemies": "Список врагов", "listEnemies": "Список врагов",
"turbulenceBuff": "Бафф турбулентности", "turbulenceBuff": "Бафф турбулентности",
"noEventSelected": "Событие не выбрано", "noEventSelected": "Событие не выбрано",
"noTurbulenceBuff": "Нет баффа турбулентности", "noTurbulenceBuff": "Нет баффа турбулентности",
"upper": "Верх", "upper": "Верх",
"lower": "Низ", "lower": "Низ",
"upperToLower": "Верх -> Низ", "upperToLower": "Верх -> Низ",
"lowerToUpper": "Низ -> Верх", "lowerToUpper": "Низ -> Верх",
"selectMOCEvent": "Выбрать событие MOC", "selectMOCEvent": "Выбрать событие MOC",
"selectPFEvent": "Выбрать событие PF", "selectPFEvent": "Выбрать событие PF",
"selectASEvent": "Выбрать событие AS", "selectASEvent": "Выбрать событие AS",
"selectCEEvent": "Выбрать событие CE", "selectCEEvent": "Выбрать событие CE",
"selectEvent": "Выбрать событие", "selectEvent": "Выбрать событие",
"selectFloor": "Выбрать этаж", "selectFloor": "Выбрать этаж",
"selectSide": "Выбрать половину", "selectSide": "Выбрать половину",
"selectBuff": "Выбрать бафф", "selectBuff": "Выбрать бафф",
"selectStage": "Выбрать стадию", "selectStage": "Выбрать стадию",
"previous": "Назад", "previous": "Назад",
"next": "Вперед", "next": "Вперед",
"noMonstersFound": "Монстры не найдены", "noMonstersFound": "Монстры не найдены",
"addNewWave": "Добавить новую волну", "addNewWave": "Добавить новую волну",
"searchStage": "Поиск стадии...", "searchStage": "Поиск стадии...",
"noStageFound": "Стадия не найдена", "noStageFound": "Стадия не найдена",
"searchMonster": "Поиск монстра...", "searchMonster": "Поиск монстра...",
"changeRelic": "Изменить реликвию", "changeRelic": "Изменить реликвию",
"deleteRelic": "Удалить реликвию", "deleteRelic": "Удалить реликвию",
"deleteRelicConfirm": "Удалить реликвию в этом слоте?", "deleteRelicConfirm": "Удалить реликвию в этом слоте?",
"setEffects": "Настроить эффекты", "setEffects": "Настроить эффекты",
"details": "Детали", "details": "Детали",
"normal": "Базовая атака", "normal": "Базовая атака",
"bpskill": "Навык", "bpskill": "Навык",
"maze": "Техника", "maze": "Техника",
"ultra": "Сверхспособность", "ultra": "Сверхспособность",
"servantskill": "Навык мемоспрайта", "assist": "Навык поддержки",
"severaltalent": "Талант мемоспрайта", "servantskill": "Навык мемоспрайта",
"singleattack": "Одиночная атака", "severaltalent": "Талант мемоспрайта",
"enhance": "Усиление", "singleattack": "Одиночная атака",
"summon": "Призыв", "enhance": "Усиление",
"mazeattack": "Атака в технике", "summon": "Призыв",
"blast": "Взрыв", "mazeattack": "Атака в технике",
"restore": "Восстановление", "blast": "Взрыв",
"support": "Поддержка", "restore": "Восстановление",
"aoeattack": "АоЕ атака", "support": "Поддержка",
"impair": "Ослабление", "aoeattack": "АоЕ атака",
"bounce": "Отскок", "impair": "Ослабление",
"active": "Активен", "bounce": "Отскок",
"defence": "Защита", "active": "Активен",
"inactive": "Неактивен", "defence": "Защита",
"maxAll": "Улучшить всё", "inactive": "Неактивен",
"maxAllSuccess": "Уровни навыков улучшены до максимума.", "maxAll": "Улучшить всё",
"maxAllFailed": "Не удалось улучшить навыки.", "maxAllSuccess": "Уровни навыков улучшены до максимума.",
"noRelicEquipped": "Нет экипированных реликвий", "maxAllFailed": "Не удалось улучшить навыки.",
"anomalyArbitration": "Аномальный арбитраж", "noRelicEquipped": "Нет экипированных реликвий",
"normalMode": "Обычный режим", "anomalyArbitration": "Аномальный арбитраж",
"hardMode": "Сложный режим", "normalMode": "Обычный режим",
"selectPEAKEvent": "Выбрать событие PEAK", "hardMode": "Сложный режим",
"mode": "Режим", "selectPEAKEvent": "Выбрать событие PEAK",
"selectMode": "Выбрать режим", "mode": "Режим",
"rollBack": "Отменить шаг", "selectMode": "Выбрать режим",
"upRoll": "Повысить улучшение", "rollBack": "Отменить шаг",
"downRoll": "Понизить улучшение", "upRoll": "Повысить улучшение",
"actions": "Действия", "downRoll": "Понизить улучшение",
"avatars": "Персонажи", "actions": "Действия",
"quickView": "Быстрый просмотр", "avatars": "Персонажи",
"extraSetting": "Дополнительные настройки", "quickView": "Быстрый просмотр",
"disableCensorship": "Отключить цензуру", "extraSetting": "Дополнительные настройки",
"hideUI": "Скрыть интерфейс", "disableCensorship": "Отключить цензуру",
"theoryCraftMode": "Режим Theorycraft", "hideUI": "Скрыть интерфейс",
"cycleCount": "Счетчик циклов", "theoryCraftMode": "Режим Theorycraft",
"pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы", "cycleCount": "Счетчик циклов",
"subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем", "pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем",
"multipathCharacter": "Многопутевой персонаж", "theoryCraft": "Theorycraft",
"mainPath": "Основной Путь", "multipathCharacter": "Многопутевой персонаж",
"march7Path": "Путь Март 7", "mainPath": "Основной Путь",
"challenge": "Испытание", "march7Path": "Путь Март 7",
"skipNode": "Пропустить узел", "challenge": "Испытание",
"disableSkip": "Отключить пропуск", "skipNode": "Пропустить узел",
"skipNode1": "Пропустить узел 1", "disableSkip": "Отключить пропуск",
"skipNode2": "Пропустить узел 2", "skipNode1": "Пропустить узел 1",
"extraFeatures": "Дополнительные функции", "skipNode2": "Пропустить узел 2",
"detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.", "extraFeatures": "Дополнительные функции",
"detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.", "detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.",
"detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.", "detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.",
"detailHiddenUi": "Скрывает игровой интерфейс.", "detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.",
"detailDisableCensorship": "Отключает внутриигровую цензуру.", "detailHiddenUi": "Скрывает игровой интерфейс.",
"detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.", "detailDisableCensorship": "Отключает внутриигровую цензуру.",
"trailblazer": "Первопроходец", "detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.",
"listExtraEffect": "Список доп. эффектов", "trailblazer": "Первопроходец",
"extra": "Экстра", "listExtraEffect": "Список доп. эффектов",
"customLineup": "Пользовательский отряд" "extra": "Экстра",
} "customLineup": "Пользовательский отряд",
} "def": "Защ",
"language": "Язык"
}
}
+293 -290
View File
@@ -1,290 +1,293 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "เครื่องมือ Firefly โดย Firefly Shelter" "description": "เครื่องมือ Firefly โดย Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "ประเภทสกิล", "skillType": "ประเภทสกิล",
"skillName": "ชื่อสกิล", "skillName": "ชื่อสกิล",
"character": "ตัวละคร", "character": "ตัวละคร",
"id": "ไอดี", "id": "ไอดี",
"path": "Path", "path": "Path",
"rarity": "ระดับความหายาก", "rarity": "ระดับความหายาก",
"element": "ธาตุ", "element": "ธาตุ",
"technique": "เทคนิค", "technique": "เทคนิค",
"talent": "พรสวรรค์", "talent": "พรสวรรค์",
"basic": "โจมตีปกติ", "basic": "โจมตีปกติ",
"skill": "สกิลต่อสู้", "skill": "สกิลต่อสู้",
"ultimate": "ท่าไม้ตาย", "ultimate": "ท่าไม้ตาย",
"servant": "ผู้ติดตาม", "servant": "ผู้ติดตาม",
"damage": "ความเสียหาย", "damage": "ความเสียหาย",
"type": "ประเภท", "type": "ประเภท",
"warrior": "ทำลายล้าง", "warrior": "ทำลายล้าง",
"knight": "อนุรักษ์", "knight": "อนุรักษ์",
"mage": "ปัญญา", "mage": "ปัญญา",
"priest": "เฟื่องฟู", "priest": "เฟื่องฟู",
"rogue": "ล่าสังหาร", "rogue": "ล่าสังหาร",
"shaman": "ประสาน", "shaman": "ประสาน",
"warlock": "ลบล้าง", "warlock": "ลบล้าง",
"memory": "ลบล้าง", "memory": "ลบล้าง",
"elation": "ปิติสุข", "elation": "ปิติสุข",
"fire": "ไฟ", "fire": "ไฟ",
"ice": "น้ำแข็ง", "ice": "น้ำแข็ง",
"imaginary": "จินตภาพ", "imaginary": "จินตภาพ",
"physical": "กายภาพ", "physical": "กายภาพ",
"quantum": "ควอนตัม", "quantum": "ควอนตัม",
"thunder": "สายฟ้า", "thunder": "สายฟ้า",
"wind": "ลม", "wind": "ลม",
"hp": "HP", "hp": "HP",
"atk": "ATK", "atk": "ATK",
"speed": "ความเร็ว", "speed": "ความเร็ว",
"critRate": "อัตราคริติคอล", "critRate": "อัตราคริติคอล",
"critDmg": "ความเสียหายคริติคอล", "critDmg": "ความเสียหายคริติคอล",
"breakEffect": "เอฟเฟกต์ทำลายล้าง", "breakEffect": "เอฟเฟกต์ทำลายล้าง",
"effectRes": "ต้านทานสถานะ", "effectRes": "ต้านทานสถานะ",
"energyRegenerationRate": "อัตราฟื้นฟูพลังงาน", "energyRegenerationRate": "อัตราฟื้นฟูพลังงาน",
"effectHitRate": "อัตราสร้างสถานะ", "effectHitRate": "อัตราสร้างสถานะ",
"outgoingHealingBoost": "โบนัสการรักษา", "outgoingHealingBoost": "โบนัสการรักษา",
"fireDmgBoost": "โบนัสความเสียหายไฟ", "fireDmgBoost": "โบนัสความเสียหายไฟ",
"iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง", "iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง",
"imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ", "imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ",
"physicalDmgBoost": "โบนัสความเสียหายกายภาพ", "physicalDmgBoost": "โบนัสความเสียหายกายภาพ",
"quantumDmgBoost": "โบนัสความเสียหายควอนตัม", "quantumDmgBoost": "โบนัสความเสียหายควอนตัม",
"thunderDmgBoost": "โบนัสความเสียหายสายฟ้า", "thunderDmgBoost": "โบนัสความเสียหายสายฟ้า",
"windDmgBoost": "โบนัสความเสียหายลม", "windDmgBoost": "โบนัสความเสียหายลม",
"pursued": "ความเสียหายเพิ่มเติม", "pursued": "ความเสียหายเพิ่มเติม",
"true damage": "ความเสียหายจริง", "true damage": "ความเสียหายจริง",
"elationdamage": "ความเสียหายปิติสุข", "elationdamage": "ความเสียหายปิติสุข",
"follow-up": "ความเสียหายโจมตีต่อเนื่อง", "follow-up": "ความเสียหายโจมตีต่อเนื่อง",
"elemental damage": "ความเสียหายทำลายล้างและซูเปอร์", "elemental damage": "ความเสียหายทำลายล้างและซูเปอร์",
"dot": "ความเสียหายต่อเนื่อง", "dot": "ความเสียหายต่อเนื่อง",
"qte": "สกิล QTE", "qte": "สกิล QTE",
"level": "เลเวล", "level": "เลเวล",
"relics": "รีลิกส์", "relics": "รีลิกส์",
"eidolons": "Eidolon", "eidolons": "Eidolon",
"lightcones": "Light Cone", "lightcones": "Light Cone",
"loadData": "โหลดข้อมูล", "loadData": "โหลดข้อมูล",
"exportData": "ส่งออกข้อมูล", "exportData": "ส่งออกข้อมูล",
"connectSetting": "การตั้งค่าการเชื่อมต่อ", "connectSetting": "การตั้งค่าการเชื่อมต่อ",
"connected": "เชื่อมต่อแล้ว", "connected": "เชื่อมต่อแล้ว",
"unconnected": "ไม่ได้เชื่อมต่อ", "unconnected": "ไม่ได้เชื่อมต่อ",
"psConnection": "การเชื่อมต่อ PS", "psConnection": "การเชื่อมต่อ PS",
"connectionType": "ประเภทการเชื่อมต่อ", "connectionType": "ประเภทการเชื่อมต่อ",
"status": "สถานะ", "status": "สถานะ",
"connectPs": "เชื่อมต่อ PS", "connectPs": "เชื่อมต่อ PS",
"disconnect": "ยกเลิกการเชื่อมต่อ", "disconnect": "ยกเลิกการเชื่อมต่อ",
"other": "อื่นๆ", "other": "อื่นๆ",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "ฐานข้อมูล", "database": "ฐานข้อมูล",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "ตั้งค่ามอนสเตอร์", "monsterSetting": "ตั้งค่ามอนสเตอร์",
"serverUrl": "URL เซิร์ฟเวอร์", "serverUrl": "URL เซิร์ฟเวอร์",
"privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว", "privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว",
"local": "ในเครื่อง", "local": "ในเครื่อง",
"server": "เซิร์ฟเวอร์", "server": "เซิร์ฟเวอร์",
"username": "ชื่อผู้ใช้", "username": "ชื่อผู้ใช้",
"password": "รหัสผ่าน", "password": "รหัสผ่าน",
"placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์", "placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์",
"placeholderUsername": "ป้อนชื่อผู้ใช้", "placeholderUsername": "ป้อนชื่อผู้ใช้",
"placeholderPassword": "ป้อนรหัสผ่าน", "placeholderPassword": "ป้อนรหัสผ่าน",
"connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ", "connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ",
"connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS", "connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS",
"syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ", "syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ",
"syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS", "syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS",
"sync": "ซิงค์", "sync": "ซิงค์",
"importSetting": "การตั้งค่าการนำเข้า", "importSetting": "การตั้งค่าการนำเข้า",
"profile": "โปรไฟล์", "profile": "โปรไฟล์",
"default": "ค่าเริ่มต้น", "default": "ค่าเริ่มต้น",
"copyProfiles": "คัดลอกโปรไฟล์", "copyProfiles": "คัดลอกโปรไฟล์",
"addNewProfile": "เพิ่มโปรไฟล์ใหม่", "addNewProfile": "เพิ่มโปรไฟล์ใหม่",
"createNewProfile": "สร้างโปรไฟล์ใหม่", "createNewProfile": "สร้างโปรไฟล์ใหม่",
"editProfile": "แก้ไขโปรไฟล์", "editProfile": "แก้ไขโปรไฟล์",
"placeholderProfileName": "ป้อนชื่อโปรไฟล์", "placeholderProfileName": "ป้อนชื่อโปรไฟล์",
"profileName": "ชื่อโปรไฟล์", "profileName": "ชื่อโปรไฟล์",
"create": "สร้าง", "create": "สร้าง",
"update": "อัปเดต", "update": "อัปเดต",
"characterInformation": "ข้อมูลตัวละคร", "characterInformation": "ข้อมูลตัวละคร",
"skills": "สกิล", "skills": "สกิล",
"showcaseCard": "การ์ดแสดง", "showcaseCard": "การ์ดแสดง",
"comingSoon": "เร็วๆ นี้", "comingSoon": "เร็วๆ นี้",
"characterName": "ชื่อตัวละคร", "characterName": "ชื่อตัวละคร",
"placeholderCharacter": "ป้อนชื่อตัวละคร", "placeholderCharacter": "ป้อนชื่อตัวละคร",
"characterSettings": "การตั้งค่าตัวละคร", "characterSettings": "การตั้งค่าตัวละคร",
"levelConfiguration": "การกำหนดระดับเลเวล", "levelConfiguration": "การกำหนดระดับเลเวล",
"characterLevel": "เลเวลตัวละคร", "characterLevel": "เลเวลตัวละคร",
"max": "สูงสุด", "max": "สูงสุด",
"ultimateEnergy": "พลังงานท่าไม้ตาย", "ultimateEnergy": "พลังงานท่าไม้ตาย",
"currentEnergy": "พลังงานปัจจุบัน", "currentEnergy": "พลังงานปัจจุบัน",
"setTo50": "ตั้งค่าเป็น 50%", "setTo50": "ตั้งค่าเป็น 50%",
"battleConfiguration": "การกำหนดค่าการต่อสู้", "battleConfiguration": "การกำหนดค่าการต่อสู้",
"useTechnique": "ใช้เทคนิค", "useTechnique": "ใช้เทคนิค",
"techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้", "techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้",
"enhancement": "การเสริมพลัง", "enhancement": "การเสริมพลัง",
"enhancementLevel": "ระดับการเสริมพลัง", "enhancementLevel": "ระดับการเสริมพลัง",
"origin": "ดั้งเดิม", "origin": "ดั้งเดิม",
"enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ", "enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ",
"lightconeEquipment": "สวมใส่ Light Cone", "lightconeEquipment": "สวมใส่ Light Cone",
"lightconeSettings": "การตั้งค่า Light Cone", "lightconeSettings": "การตั้งค่า Light Cone",
"placeholderLevel": "ป้อนเลเวล", "placeholderLevel": "ป้อนเลเวล",
"superimpositionRank": "ระดับการขัดเกลา", "superimpositionRank": "ระดับการขัดเกลา",
"ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า", "ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า",
"changeLightcone": "เปลี่ยน Light Cone", "changeLightcone": "เปลี่ยน Light Cone",
"removeLightcone": "ถอด Light Cone", "removeLightcone": "ถอด Light Cone",
"equipLightcone": "สวมใส่ Light Cone", "equipLightcone": "สวมใส่ Light Cone",
"noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone", "noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone",
"equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง", "equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง",
"filter": "กรอง", "filter": "กรอง",
"selectedCharacters": "ตัวละครที่เลือก", "selectedCharacters": "ตัวละครที่เลือก",
"selectedProfiles": "โปรไฟล์ที่เลือก", "selectedProfiles": "โปรไฟล์ที่เลือก",
"clearAll": "ล้างทั้งหมด", "clearAll": "ล้างทั้งหมด",
"selectAll": "เลือกทั้งหมด", "selectAll": "เลือกทั้งหมด",
"copy": "คัดลอก", "copy": "คัดลอก",
"copied": "คัดลอกแล้ว", "copied": "คัดลอกแล้ว",
"noAvatarSelected": "ไม่ได้เลือกตัวละคร", "noAvatarSelected": "ไม่ได้เลือกตัวละคร",
"noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก", "noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก",
"pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์", "pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์",
"pleaseEnterUid": "โปรดป้อน UID", "pleaseEnterUid": "โปรดป้อน UID",
"failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้", "failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้",
"pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร", "pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร",
"noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า", "noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า",
"pleaseSelectAFile": "โปรดเลือกไฟล์", "pleaseSelectAFile": "โปรดเลือกไฟล์",
"fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง", "fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง",
"importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ", "importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ",
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ", "importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ", "importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
"getData": "รับข้อมูล", "getData": "รับข้อมูล",
"import": "นำเข้า", "import": "นำเข้า",
"freeSRImport": "นำเข้า FreeSR", "freeSRImport": "นำเข้า FreeSR",
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR", "onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
"pickAFile": "เลือกไฟล์", "pickAFile": "เลือกไฟล์",
"lightConeSetting": "การตั้งค่า Light Cone", "lightConeSetting": "การตั้งค่า Light Cone",
"relicMaker": "สร้างรีลิกส์", "relicMaker": "สร้างรีลิกส์",
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด", "pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
"relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ", "relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ",
"mainSettings": "การตั้งค่าหลัก", "mainSettings": "การตั้งค่าหลัก",
"mainStat": "ค่าสเตตัสหลัก", "mainStat": "ค่าสเตตัสหลัก",
"set": "เซ็ต", "set": "เซ็ต",
"pleaseSelectASet": "โปรดเลือกเซ็ต", "pleaseSelectASet": "โปรดเลือกเซ็ต",
"effectBonus": "โบนัสเอฟเฟกต์", "effectBonus": "โบนัสเอฟเฟกต์",
"totalRoll": "จำนวนการอัปเกรด", "totalRoll": "จำนวนการอัปเกรด",
"randomizeStats": "สุ่มสเตตัส", "randomizeStats": "สุ่มสเตตัส",
"randomizeRolls": "สุ่มอัปเกรด", "randomizeRolls": "สุ่มอัปเกรด",
"selectASubStat": "เลือกสเตตัสรอง", "selectASubStat": "เลือกสเตตัสรอง",
"selectASet": "เลือกเซ็ต", "selectASet": "เลือกเซ็ต",
"selectAMainStat": "เลือกสเตตัสหลัก", "selectAMainStat": "เลือกสเตตัสหลัก",
"save": "บันทึก", "save": "บันทึก",
"reset": "รีเซ็ต", "reset": "รีเซ็ต",
"roll": "อัปเกรด", "roll": "อัปเกรด",
"step": "ขั้น", "step": "ขั้น",
"memoryOfChaos": "Memory of Chaos", "memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction", "pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow", "apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "ศัตรูกำหนดเอง", "customEnemy": "ศัตรูกำหนดเอง",
"simulatedUniverse": "Simulated Universe", "simulatedUniverse": "Simulated Universe",
"floor": "ชั้น", "floor": "ชั้น",
"side": "ฝั่ง", "side": "ฝั่ง",
"wave": "เวฟ", "wave": "เวฟ",
"stage": "ด่าน", "stage": "ด่าน",
"useCycleCount": "ใช้การนับรอบหรือไม่?", "useCycleCount": "ใช้การนับรอบหรือไม่?",
"useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?", "useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?",
"firstHalfEnemies": "ศัตรูครึ่งแรก", "firstHalfEnemies": "ศัตรูครึ่งแรก",
"secondHalfEnemies": "ศัตรูครึ่งหลัง", "secondHalfEnemies": "ศัตรูครึ่งหลัง",
"firstNodeEnemies": "ศัตรูโหนด 1", "firstNodeEnemies": "ศัตรูโหนด 1",
"secondNodeEnemies": "ศัตรูโหนด 2", "secondNodeEnemies": "ศัตรูโหนด 2",
"thirdNodeEnemies": "ศัตรูโหนด 3", "thirdNodeEnemies": "ศัตรูโหนด 3",
"firstNode": "โหนด 1", "firstNode": "โหนด 1",
"secondNode": "โหนด 2", "secondNode": "โหนด 2",
"thirdNode": "โหนด 3", "thirdNode": "โหนด 3",
"listEnemies": "รายการศัตรู", "listEnemies": "รายการศัตรู",
"turbulenceBuff": "บัฟบรรยากาศ", "turbulenceBuff": "บัฟบรรยากาศ",
"noEventSelected": "ไม่ได้เลือกอีเวนต์", "noEventSelected": "ไม่ได้เลือกอีเวนต์",
"noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ", "noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ",
"upper": "บน", "upper": "บน",
"lower": "ล่าง", "lower": "ล่าง",
"upperToLower": "บน -> ล่าง", "upperToLower": "บน -> ล่าง",
"lowerToUpper": "ล่าง -> บน", "lowerToUpper": "ล่าง -> บน",
"selectMOCEvent": "เลือกอีเวนต์ MOC", "selectMOCEvent": "เลือกอีเวนต์ MOC",
"selectPFEvent": "เลือกอีเวนต์ PF", "selectPFEvent": "เลือกอีเวนต์ PF",
"selectASEvent": "เลือกอีเวนต์ AS", "selectASEvent": "เลือกอีเวนต์ AS",
"selectCEEvent": "เลือกอีเวนต์ CE", "selectCEEvent": "เลือกอีเวนต์ CE",
"selectEvent": "เลือกอีเวนต์", "selectEvent": "เลือกอีเวนต์",
"selectFloor": "เลือกชั้น", "selectFloor": "เลือกชั้น",
"selectSide": "เลือกฝั่ง", "selectSide": "เลือกฝั่ง",
"selectBuff": "เลือกบัฟ", "selectBuff": "เลือกบัฟ",
"selectStage": "เลือกด่าน", "selectStage": "เลือกด่าน",
"previous": "ก่อนหน้า", "previous": "ก่อนหน้า",
"next": "ถัดไป", "next": "ถัดไป",
"noMonstersFound": "ไม่พบมอนสเตอร์", "noMonstersFound": "ไม่พบมอนสเตอร์",
"addNewWave": "เพิ่มเวฟใหม่", "addNewWave": "เพิ่มเวฟใหม่",
"searchStage": "ค้นหาด่าน...", "searchStage": "ค้นหาด่าน...",
"noStageFound": "ไม่พบด่าน", "noStageFound": "ไม่พบด่าน",
"searchMonster": "ค้นหามอนสเตอร์...", "searchMonster": "ค้นหามอนสเตอร์...",
"changeRelic": "เปลี่ยนรีลิกส์", "changeRelic": "เปลี่ยนรีลิกส์",
"deleteRelic": "ลบรีลิกส์", "deleteRelic": "ลบรีลิกส์",
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?", "deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
"setEffects": "ตั้งค่าเอฟเฟกต์", "setEffects": "ตั้งค่าเอฟเฟกต์",
"details": "รายละเอียด", "details": "รายละเอียด",
"normal": "โจมตีปกติ", "normal": "โจมตีปกติ",
"bpskill": "สกิลต่อสู้", "bpskill": "สกิลต่อสู้",
"maze": "เทคนิค", "maze": "เทคนิค",
"ultra": "ท่าไม้ตาย", "ultra": "ท่าไม้ตาย",
"servantskill": "สกิล Memosprite", "assist": "สกิลช่วยเหลือ",
"severaltalent": "พรสวรรค์ Memosprite", "servantskill": "สกิล Memosprite",
"singleattack": "โจมตีเป้าหมายเดียว", "severaltalent": "พรสวรรค์ Memosprite",
"enhance": "เสริมพลัง", "singleattack": "โจมตีเป้าหมายเดียว",
"summon": "อัญเชิญ", "enhance": "เสริมพลัง",
"mazeattack": "โจมตีแบบเทคนิค", "summon": "อัญเชิญ",
"blast": "กระจาย", "mazeattack": "โจมตีแบบเทคนิค",
"restore": "ฟื้นฟู", "blast": "กระจาย",
"support": "สนับสนุน", "restore": "ฟื้นฟู",
"aoeattack": "โจมตีหมู่", "support": "สนับสนุน",
"impair": "ป่วน", "aoeattack": "โจมตีหมู่",
"bounce": "ชิ่ง", "impair": "ป่วน",
"active": "เปิดใช้งาน", "bounce": "ชิ่ง",
"defence": "ป้องกัน", "active": "เปิดใช้งาน",
"inactive": "ปิดใช้งาน", "defence": "ป้องกัน",
"maxAll": "อัปเกรดทั้งหมด", "inactive": "ปิดใช้งาน",
"maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ", "maxAll": "อัปเกรดทั้งหมด",
"maxAllFailed": "อัปเกรดสกิลล้มเหลว", "maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ",
"noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์", "maxAllFailed": "อัปเกรดสกิลล้มเหลว",
"anomalyArbitration": "Anomaly Arbitration", "noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์",
"normalMode": "โหมดปกติ", "anomalyArbitration": "Anomaly Arbitration",
"hardMode": "โหมดยาก", "normalMode": "โหมดปกติ",
"selectPEAKEvent": "เลือกอีเวนต์ PEAK", "hardMode": "โหมดยาก",
"mode": "โหมด", "selectPEAKEvent": "เลือกอีเวนต์ PEAK",
"selectMode": "เลือกโหมด", "mode": "โหมด",
"rollBack": "ย้อนกลับ", "selectMode": "เลือกโหมด",
"upRoll": "เพิ่มอัปเกรด", "rollBack": "ย้อนกลับ",
"downRoll": "ลดอัปเกรด", "upRoll": "เพิ่มอัปเกรด",
"actions": "แอ็กชัน", "downRoll": "ลดอัปเกรด",
"avatars": "อวตาร", "actions": "แอ็กชัน",
"quickView": "ดูแบบด่วน", "avatars": "อวตาร",
"extraSetting": "การตั้งค่าเพิ่มเติม", "quickView": "ดูแบบด่วน",
"disableCensorship": "ปิดการเซ็นเซอร์", "extraSetting": "การตั้งค่าเพิ่มเติม",
"hideUI": "ซ่อน UI", "disableCensorship": "ปิดการเซ็นเซอร์",
"theoryCraftMode": "โหมด Theorycraft", "hideUI": "ซ่อน UI",
"cycleCount": "จำนวนรอบ", "theoryCraftMode": "โหมด Theorycraft",
"pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด", "cycleCount": "จำนวนรอบ",
"subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์", "pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด",
"theoryCraft": "Theorycraft", "subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์",
"multipathCharacter": "ตัวละครหลาย Path", "theoryCraft": "Theorycraft",
"mainPath": "Path หลัก", "multipathCharacter": "ตัวละครหลาย Path",
"march7Path": "Path March 7th", "mainPath": "Path หลัก",
"challenge": "ท้าทาย", "march7Path": "Path March 7th",
"skipNode": "ข้ามโหนด", "challenge": "ท้าทาย",
"disableSkip": "ปิดใช้งานการข้าม", "skipNode": "ข้ามโหนด",
"skipNode1": "ข้ามโหนด 1", "disableSkip": "ปิดใช้งานการข้าม",
"skipNode2": "ข้ามโหนด 2", "skipNode1": "ข้ามโหนด 1",
"extraFeatures": "คุณสมบัติพิเศษ", "skipNode2": "ข้ามโหนด 2",
"detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้", "extraFeatures": "คุณสมบัติพิเศษ",
"detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction", "detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้",
"detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน", "detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction",
"detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม", "detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน",
"detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม", "detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม",
"detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว", "detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม",
"trailblazer": "ผู้บุกเบิก", "detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ", "trailblazer": "ผู้บุกเบิก",
"extra": "พิเศษ", "listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง" "extra": "พิเศษ",
} "customLineup": "จัดทีมแบบกำหนดเอง",
} "def": "DEF",
"language": "ภาษา"
}
}
+292 -289
View File
@@ -1,289 +1,292 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Tools", "title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter" "description": "Firefly tools by Firefly Shelter"
}, },
"DataPage": { "DataPage": {
"skillType": "Loại kỹ năng", "skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng", "skillName": "Tên kỹ năng",
"character": "Nhân vật", "character": "Nhân vật",
"id": "ID", "id": "ID",
"path": "Vận mệnh", "path": "Vận mệnh",
"rarity": "Độ hiếm", "rarity": "Độ hiếm",
"element": "Nguyên tố", "element": "Nguyên tố",
"technique": "Bí kỹ", "technique": "Bí kỹ",
"talent": "Thiên phú", "talent": "Thiên phú",
"basic": "Đánh thường", "basic": "Đánh thường",
"skill": "Kỹ năng", "skill": "Kỹ năng",
"ultimate": "Tuyệt kỹ", "ultimate": "Tuyệt kỹ",
"servant": "Phụ trợ", "servant": "Phụ trợ",
"damage": "Sát thương", "damage": "Sát thương",
"type": "Loại", "type": "Loại",
"warrior": "Hủy Diệt", "warrior": "Hủy Diệt",
"knight": "Bảo Hộ", "knight": "Bảo Hộ",
"mage": "Tri Thức", "mage": "Tri Thức",
"priest": "Trù phú", "priest": "Trù phú",
"rogue": "Săn Bắn", "rogue": "Săn Bắn",
"shaman": "Hòa Hợp", "shaman": "Hòa Hợp",
"warlock": "Hư Vô", "warlock": "Hư Vô",
"memory": "Ký Ức", "memory": "Ký Ức",
"elation": "Vui vẻ", "elation": "Vui vẻ",
"fire": "Hỏa", "fire": "Hỏa",
"ice": "Băng", "ice": "Băng",
"imaginary": "Ảo Ảnh", "imaginary": "Ảo Ảnh",
"physical": "Vật Lý", "physical": "Vật Lý",
"quantum": "Lượng Tử", "quantum": "Lượng Tử",
"thunder": "Lôi", "thunder": "Lôi",
"wind": "Phong", "wind": "Phong",
"hp": "HP", "hp": "HP",
"atk": "Tấn công", "atk": "Tấn công",
"speed": "Tốc độ", "speed": "Tốc độ",
"critRate": "Tỷ lệ bạo", "critRate": "Tỷ lệ bạo",
"critDmg": "ST bạo", "critDmg": "ST bạo",
"breakEffect": "sát thương kích phá", "breakEffect": "sát thương kích phá",
"effectRes": "Kháng hiệu ứng", "effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Tốc độ hồi năng lượng", "energyRegenerationRate": "Tốc độ hồi năng lượng",
"effectHitRate": "Tỷ lệ trúng hiệu ứng", "effectHitRate": "Tỷ lệ trúng hiệu ứng",
"outgoingHealingBoost": "Tăng hồi phục", "outgoingHealingBoost": "Tăng hồi phục",
"fireDmgBoost": "Tăng sát thương Hỏa", "fireDmgBoost": "Tăng sát thương Hỏa",
"iceDmgBoost": "Tăng sát thương Băng", "iceDmgBoost": "Tăng sát thương Băng",
"imaginaryDmgBoost": "Tăng sát thương Ảo Ảnh", "imaginaryDmgBoost": "Tăng sát thương Ảo Ảnh",
"physicalDmgBoost": "Tăng sát thương Vật Lý", "physicalDmgBoost": "Tăng sát thương Vật Lý",
"quantumDmgBoost": "Tăng sát thương Lượng Tử", "quantumDmgBoost": "Tăng sát thương Lượng Tử",
"thunderDmgBoost": "Tăng sát thương Lôi", "thunderDmgBoost": "Tăng sát thương Lôi",
"windDmgBoost": "Tăng sát thương Phong", "windDmgBoost": "Tăng sát thương Phong",
"pursued": "Sát thương thêm", "pursued": "Sát thương thêm",
"true damage": "Sát thương chuẩn", "true damage": "Sát thương chuẩn",
"elationdamage": "Sát thương vui vẻ", "elationdamage": "Sát thương vui vẻ",
"follow-up": "Sát thương phản kích", "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á", "elemental damage": "Sát thương kích phá và siêu kích phá",
"dot": "Sát thương theo thời gian", "dot": "Sát thương theo thời gian",
"qte": "Kỹ năng QTE", "qte": "Kỹ năng QTE",
"level": "Cấp độ", "level": "Cấp độ",
"relics": "Thánh di vật", "relics": "Thánh di vật",
"eidolons": "Tinh hồn", "eidolons": "Tinh hồn",
"lightcones": "Nón ánh sáng", "lightcones": "Nón ánh sáng",
"loadData": "Tải dữ liệu", "loadData": "Tải dữ liệu",
"exportData": "Xuất dữ liệu", "exportData": "Xuất dữ liệu",
"connectSetting": "Cài đặt kết nối", "connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối", "connected": "Đã kết nối",
"unconnected": "Chưa kết nối", "unconnected": "Chưa kết nối",
"psConnection": "Kết nối PS", "psConnection": "Kết nối PS",
"connectionType": "Loại kết nối", "connectionType": "Loại kết nối",
"status": "Trạng thái", "status": "Trạng thái",
"connectPs": "Kết nối PS", "connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối", "disconnect": "Ngắt kết nối",
"other": "Khác", "other": "Khác",
"freeSr": "FreeSR", "freeSr": "FreeSR",
"database": "Database", "database": "Database",
"enka": "Enka", "enka": "Enka",
"monsterSetting": "Cài đặt quái", "monsterSetting": "Cài đặt quái",
"serverUrl": "Địa chỉ server", "serverUrl": "Địa chỉ server",
"privateType": "Loại riêng tư", "privateType": "Loại riêng tư",
"local": "Cục bộ", "local": "Cục bộ",
"server": "Máy chủ", "server": "Máy chủ",
"username": "Tên người dùng", "username": "Tên người dùng",
"password": "Mật khẩu", "password": "Mật khẩu",
"placeholderServerUrl": "Nhập địa chỉ server", "placeholderServerUrl": "Nhập địa chỉ server",
"placeholderUsername": "Nhập tên người dùng", "placeholderUsername": "Nhập tên người dùng",
"placeholderPassword": "Nhập mật khẩu", "placeholderPassword": "Nhập mật khẩu",
"connectedSuccess": "Kết nối PS thành công", "connectedSuccess": "Kết nối PS thành công",
"connectedFailed": "Kết nối PS thất bại", "connectedFailed": "Kết nối PS thất bại",
"syncSuccess": "Đồng bộ dữ liệu với PS thành công", "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", "syncFailed": "Đồng bộ dữ liệu với PS thất bại",
"sync": "Đồng bộ", "sync": "Đồng bộ",
"importSetting": "Cài đặt nhập", "importSetting": "Cài đặt nhập",
"profile": "Hồ sơ", "profile": "Hồ sơ",
"default": "Mặc định", "default": "Mặc định",
"copyProfiles": "Sao chép hồ sơ", "copyProfiles": "Sao chép hồ sơ",
"addNewProfile": "Thêm hồ sơ mới", "addNewProfile": "Thêm hồ sơ mới",
"createNewProfile": "Tạo hồ sơ mới", "createNewProfile": "Tạo hồ sơ mới",
"editProfile": "Chỉnh sửa hồ sơ", "editProfile": "Chỉnh sửa hồ sơ",
"placeholderProfileName": "Nhập tên hồ sơ", "placeholderProfileName": "Nhập tên hồ sơ",
"profileName": "Tên hồ sơ", "profileName": "Tên hồ sơ",
"create": "Tạo", "create": "Tạo",
"update": "Cập nhật", "update": "Cập nhật",
"characterInformation": "Thông tin nhân vật", "characterInformation": "Thông tin nhân vật",
"skills": "Kỹ năng", "skills": "Kỹ năng",
"showcaseCard": "Thẻ trưng bày", "showcaseCard": "Thẻ trưng bày",
"comingSoon": "Sắp ra mắt", "comingSoon": "Sắp ra mắt",
"characterName": "Tên nhân vật", "characterName": "Tên nhân vật",
"placeholderCharacter": "Nhập tên nhân vật", "placeholderCharacter": "Nhập tên nhân vật",
"characterSettings": "Cài đặt nhân vật", "characterSettings": "Cài đặt nhân vật",
"levelConfiguration": "Cấu hình cấp độ", "levelConfiguration": "Cấu hình cấp độ",
"characterLevel": "Cấp độ nhân vật", "characterLevel": "Cấp độ nhân vật",
"max": "Tối đa", "max": "Tối đa",
"ultimateEnergy": "Năng lượng tuyệt kỹ", "ultimateEnergy": "Năng lượng tuyệt kỹ",
"currentEnergy": "Năng lượng hiện tại", "currentEnergy": "Năng lượng hiện tại",
"setTo50": "Đặt thành 50%", "setTo50": "Đặt thành 50%",
"battleConfiguration": "Cấu hình trận đấu", "battleConfiguration": "Cấu hình trận đấu",
"useTechnique": "Dùng bí kỹ", "useTechnique": "Dùng bí kỹ",
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận", "techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
"enhancement": "Cường hóa", "enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa", "enhancementLevel": "Cấp độ cường hóa",
"origin": "Nguyên gốc", "origin": "Nguyên gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng", "enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng", "lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng", "lightconeSettings": "Cài đặt nón ánh sáng",
"placeholderLevel": "Nhập cấp độ", "placeholderLevel": "Nhập cấp độ",
"superimpositionRank": "Bậc chồng kỹ năng", "superimpositionRank": "Tích tầng",
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh", "ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
"changeLightcone": "Thay đổi nón ánh sáng", "changeLightcone": "Thay đổi nón ánh sáng",
"removeLightcone": "Gỡ nón ánh sáng", "removeLightcone": "Gỡ nón ánh sáng",
"equipLightcone": "Trang bị nón ánh sáng", "equipLightcone": "Trang bị nón ánh sáng",
"noLightconeEquipped": "Chưa 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", "equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
"filter": "Lọc", "filter": "Lọc",
"selectedCharacters": "Nhân vật đã chọn", "selectedCharacters": "Nhân vật đã chọn",
"selectedProfiles": "Hồ sơ đã chọn", "selectedProfiles": "Hồ sơ đã chọn",
"clearAll": "Xóa tất cả", "clearAll": "Xóa tất cả",
"selectAll": "Chọn tất cả", "selectAll": "Chọn tất cả",
"copy": "Sao chép", "copy": "Sao chép",
"copied": "Đã sao chép", "copied": "Đã sao chép",
"noAvatarSelected": "Chưa chọn nhân vật", "noAvatarSelected": "Chưa chọn nhân vật",
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép", "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ơ", "pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
"pleaseEnterUid": "Vui lòng nhập UID", "pleaseEnterUid": "Vui lòng nhập UID",
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại", "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", "pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
"noDataToImport": "Không có dữ liệu để nhập", "noDataToImport": "Không có dữ liệu để nhập",
"pleaseSelectAFile": "Vui lòng chọn một tệp", "pleaseSelectAFile": "Vui lòng chọn một tệp",
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ", "fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công", "importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR 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", "importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
"getData": "Lấy dữ liệu", "getData": "Lấy dữ liệu",
"import": "Nhập", "import": "Nhập",
"freeSRImport": "Nhập FreeSR", "freeSRImport": "Nhập FreeSR",
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR", "onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
"pickAFile": "Chọn tệp", "pickAFile": "Chọn tệp",
"lightConeSetting": "Cài đặt Nón Ánh Sáng", "lightConeSetting": "Cài đặt Nón Ánh Sáng",
"relicMaker": "Trình tạo Thánh Di Vật", "relicMaker": "Trình tạo Thánh Di Vật",
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn", "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", "relicSavedSuccessfully": "Lưu thánh di vật thành công",
"mainSettings": "Cài đặt chính", "mainSettings": "Cài đặt chính",
"mainStat": "Chỉ số chính", "mainStat": "Chỉ số chính",
"set": "Bộ", "set": "Bộ",
"pleaseSelectASet": "Vui lòng chọn một bộ", "pleaseSelectASet": "Vui lòng chọn một bộ",
"effectBonus": "Hiệu ứng cộng thêm", "effectBonus": "Hiệu ứng cộng thêm",
"totalRoll": "Tổng số dòng", "totalRoll": "Tổng số dòng",
"randomizeStats": "Ngẫu nhiên chỉ số", "randomizeStats": "Ngẫu nhiên chỉ số",
"randomizeRolls": "Ngẫu nhiên số dòng", "randomizeRolls": "Ngẫu nhiên số dòng",
"selectASubStat": "Chọn chỉ số phụ", "selectASubStat": "Chọn chỉ số phụ",
"selectASet": "Chọn một bộ", "selectASet": "Chọn một bộ",
"selectAMainStat": "Chọn chỉ số chính", "selectAMainStat": "Chọn chỉ số chính",
"save": "Lưu", "save": "Lưu",
"reset": "Đặt lại toàn bộ", "reset": "Đặt lại toàn bộ",
"roll": "Số dòng", "roll": "Số dòng",
"step": "Bước nhảy", "step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn", "memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu", "pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế", "apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh", "customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng", "simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng", "floor": "Tầng",
"side": "Nửa trận", "side": "Nửa trận",
"wave": "Đợt", "wave": "Đợt",
"stage": "Màn", "stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?", "useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?", "useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu", "firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau", "secondHalfEnemies": "Địch nửa sau",
"firstNodeEnemies": "Địch Node 1", "firstNodeEnemies": "Địch Node 1",
"secondNodeEnemies": "Địch Node 2", "secondNodeEnemies": "Địch Node 2",
"thirdNodeEnemies": "Địch Node 3", "thirdNodeEnemies": "Địch Node 3",
"firstNode": "Node 1", "firstNode": "Node 1",
"secondNode": "Node 2", "secondNode": "Node 2",
"thirdNode": "Node 3", "thirdNode": "Node 3",
"turbulenceBuff": "Buff hỗn loạn", "turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện", "noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn", "noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên", "upper": "Nửa trên",
"lower": "Nửa dưới", "lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới", "upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên", "lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC", "selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF", "selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS", "selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE", "selectCEEvent": "Chọn sự kiện CE",
"selectPEAKEvent": "Chọn sự kiện PEAK", "selectPEAKEvent": "Chọn sự kiện PEAK",
"selectEvent": "Chọn sự kiện", "selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng", "selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận", "selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff", "selectBuff": "Chọn buff",
"selectStage": "Chọn màn", "selectStage": "Chọn màn",
"previous": "Trước", "previous": "Trước",
"next": "Tiếp", "next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái", "noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới", "addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...", "searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn", "noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...", "searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật", "changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa 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?", "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", "setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết", "details": "Chi tiết",
"normal": "Đánh thường", "normal": "Đánh thường",
"bpskill": "Chiến kỹ", "bpskill": "Chiến kỹ",
"maze": "Bí kỹ", "maze": "Bí kỹ",
"ultra": "Tuyệt kỹ", "ultra": "Tuyệt kỹ",
"servantskill": "Kỹ năng vật triệu hồi", "assist": "Kỹ năng hỗ trợ",
"severaltalent": "Thiên phú vật triệu hồi", "servantskill": "Kỹ năng vật triệu hồi",
"singleattack": "Tấn công đơn", "severaltalent": "Thiên phú vật triệu hồi",
"enhance": "Cường hóa", "singleattack": "Tấn công đơn",
"summon": "Triệu hồi", "enhance": "Cường hóa",
"blast": "Tấn công 3 mục tiêu", "summon": "Triệu hồi",
"restore": "Hồi phục", "blast": "Tấn công 3 mục tiêu",
"support": "Hỗ trợ", "restore": "Hồi phục",
"aoeattack": "Tấn công đa mục tiêu", "support": "Hỗ trợ",
"mazeattack": "Bí kỹ tấn công", "aoeattack": "Tấn công đa mục tiêu",
"impair": "Suy yếu", "mazeattack": "Bí kỹ tấn công",
"bounce": "Nảy bật", "impair": "Suy yếu",
"active": "Kích hoạt", "bounce": "Nảy bật",
"inactive": "Không kích hoạt", "active": "Kích hoạt",
"defence": "Phòng thủ", "inactive": "Không kích hoạt",
"maxAll": "Tối đa tất cả", "defence": "Phòng thủ",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.", "maxAll": "Tối đa tất cả",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.", "maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"noRelicEquipped": "Không có di vật", "maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
"anomalyArbitration": "Trọng tài dị tướng", "noRelicEquipped": "Không có di vật",
"normalMode": "Chế độ thường", "anomalyArbitration": "Trọng tài dị tướng",
"hardMode": "Chế độ khó", "normalMode": "Chế độ thường",
"mode": "Chế độ", "hardMode": "Chế độ khó",
"selectMode": "Chọn chế độ", "mode": "Chế độ",
"rollBack": "Quay lại bước trước", "selectMode": "Chọn chế độ",
"upRoll": "Tăng dòng", "rollBack": "Quay lại bước trước",
"downRoll": "Giảm dòng", "upRoll": "Tăng dòng",
"actions": "Hành động", "downRoll": "Giảm dòng",
"avatars": "Nhân vật", "actions": "Hành động",
"quickView": "Xem nhanh", "avatars": "Nhân vật",
"extraSetting": "Cài đặt bổ sung", "quickView": "Xem nhanh",
"disableCensorship": "Tắt kiểm duyệt", "extraSetting": "Cài đặt bổ sung",
"hideUI": "Ẩn giao diện", "disableCensorship": "Tắt kiểm duyệt",
"theoryCraftMode": "Chế độ Theory Craft", "hideUI": "Ẩn giao diện",
"cycleCount": "Số vòng", "theoryCraftMode": "Chế độ Theory Craft",
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ", "cycleCount": "Số vòng",
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0", "pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
"theoryCraft": "Theory Craft", "subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
"multipathCharacter": "Nhân vật đa Vận Mệnh", "theoryCraft": "Theory Craft",
"mainPath": "Vận Mệnh Nhân Vật Chính", "multipathCharacter": "Nhân vật đa Vận Mệnh",
"march7Path": "Vận Mệnh March 7", "mainPath": "Vận Mệnh Nhân Vật Chính",
"challenge": "Thử thách", "march7Path": "Vận Mệnh March 7",
"skipNode": "Bỏ qua node", "challenge": "Thử thách",
"disableSkip": "Tắt bỏ qua", "skipNode": "Bỏ qua node",
"skipNode1": "Bỏ qua node 1", "disableSkip": "Tắt bỏ qua",
"skipNode2": "Bỏ qua node 2", "skipNode1": "Bỏ qua node 1",
"extraFeatures": "Tính năng bổ sung", "skipNode2": "Bỏ qua node 2",
"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.", "extraFeatures": "Tính năng bổ sung",
"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.", "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.",
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.", "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.",
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.", "detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.", "detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.", "detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
"trailblazer": "Nhà khai phá", "detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
"listExtraEffect": "Danh sách hiệu ứng bổ sung", "trailblazer": "Nhà khai phá",
"extra": "Bổ sung", "listExtraEffect": "Danh sách hiệu ứng bổ sung",
"customLineup": "Đội hình tùy chỉnh" "extra": "Bổ sung",
} "customLineup": "Đội hình tùy chỉnh",
} "def": "Phòng thủ",
"language": "Ngôn ngữ"
}
}
+5 -2
View File
@@ -225,6 +225,7 @@
"details": "详情", "details": "详情",
"normal": "普通攻击", "normal": "普通攻击",
"bpskill": "技能", "bpskill": "技能",
"assist": "协战技",
"maze": "秘技", "maze": "秘技",
"ultra": "终结技", "ultra": "终结技",
"servantskill": "忆灵技能", "servantskill": "忆灵技能",
@@ -284,6 +285,8 @@
"trailblazer": "开拓者", "trailblazer": "开拓者",
"listExtraEffect": "额外效果列表", "listExtraEffect": "额外效果列表",
"extra": "额外", "extra": "额外",
"customLineup": "自定义阵容" "customLineup": "自定义阵容",
"def": "防御力",
"language": "语言"
} }
} }
+12 -12
View File
@@ -10,15 +10,15 @@
}, },
"dependencies": { "dependencies": {
"@next/bundle-analyzer": "16.1.6", "@next/bundle-analyzer": "16.1.6",
"@tanstack/react-query": "^5.100.10", "@tanstack/react-query": "^5.102.5",
"axios": "^1.16.1", "axios": "^1.20.0",
"fast-average-color": "^9.5.2", "fast-average-color": "^9.5.2",
"framer-motion": "^12.38.0", "framer-motion": "^12.43.0",
"html2canvas-pro": "^1.6.7", "html2canvas-pro": "^1.6.7",
"lru-cache": "^11.3.6", "lru-cache": "^11.5.2",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"next-intl": "^4.12.0", "next-intl": "^4.13.7",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
@@ -27,20 +27,20 @@
"react-toastify": "^11.1.0", "react-toastify": "^11.1.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"zod": "^4.4.3", "zod": "^4.4.3",
"zustand": "^5.0.13" "zustand": "^5.0.15"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.3.0", "@tailwindcss/postcss": "^4.3.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^25.8.0", "@types/node": "^25.9.5",
"@types/react": "19.2.6", "@types/react": "19.2.6",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"baseline-browser-mapping": "^2.10.29", "baseline-browser-mapping": "^2.11.19",
"daisyui": "^5.5.19", "daisyui": "^5.7.22",
"eslint": "^9.39.4", "eslint": "^9.39.5",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.3",
"ts-to-zod": "^5.1.0", "ts-to-zod": "^5.1.0",
"typescript": "^5.9.3" "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" import { getDataCache } from "@/lib/cache/cache"
export async function GET( export async function GET(
req: Request, req: Request,
{ params }: { params: Promise<{ name: string }> } { params }: { params: Promise<{ name: string }> }
) { ) {
const { name } = await params const { name } = await params
const item = getDataCache(name) const item = getDataCache(name)
if (!item) { if (!item) {
return new Response("Not found", { status: 404 }) return new Response("Not found", { status: 404 })
} }
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "public, max-age=3600" "Cache-Control": "public, max-age=3600"
} }
if (item.type === "br") { if (item.type === "br") {
headers["Content-Encoding"] = "br" headers["Content-Encoding"] = "br"
} }
return new Response(new Uint8Array(item.buf), { headers }) return new Response(new Uint8Array(item.buf), { headers })
} }
+70 -70
View File
@@ -1,79 +1,79 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import axios from 'axios' import axios from 'axios'
import net from 'net' import net from 'net'
function isPrivateHost(hostname: string): boolean { function isPrivateHost(hostname: string): boolean {
if ( if (
hostname === 'localhost' || hostname === 'localhost' ||
hostname.startsWith('127.') || hostname.startsWith('127.') ||
hostname.startsWith('10.') || hostname.startsWith('10.') ||
hostname.startsWith('192.168.') || hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) { ) {
return true return true
} }
if (net.isIP(hostname)) { if (net.isIP(hostname)) {
return true return true
} }
return false return false
} }
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const { searchParams } = new URL(req.url) const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url") const targetUrl = searchParams.get("url")
if (!targetUrl) { if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 }) return NextResponse.json({ error: "Missing url" }, { status: 400 })
} }
const response = await fetch(targetUrl) const response = await fetch(targetUrl)
if (!response.ok) { if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 }) return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
} }
const buffer = await response.arrayBuffer() const buffer = await response.arrayBuffer()
return new NextResponse(buffer, { return new NextResponse(buffer, {
headers: { headers: {
"Content-Type": response.headers.get("content-type") || "image/png", "Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },
}) })
} catch (e: unknown) { } catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 }) return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
} }
} }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()
const { serverUrl, method, ...payload } = body const { serverUrl, method, ...payload } = body
if (!serverUrl) { if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 }) return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
} }
if (!method) { if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 }) return NextResponse.json({ error: 'Missing method' }, { status: 400 })
} }
let url = serverUrl.trim() let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) { if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}` url = `http://${url}`
} }
const parsed = new URL(url) const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) { if (isPrivateHost(parsed.hostname)) {
return NextResponse.json( return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` }, { error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 } { status: 403 }
) )
} }
let response let response
switch (method.toUpperCase()) { switch (method.toUpperCase()) {
+9 -9
View File
@@ -1,9 +1,9 @@
"use client" "use client"
import EidolonsInfo from "@/components/eidolonsInfo"; import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() { export default function EidolonsInfoPage() {
return ( return (
<div className="w-full"> <div className="w-full">
<EidolonsInfo/> <EidolonsInfo/>
</div> </div>
); );
} }
+80 -8
View File
@@ -1,6 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui" { @plugin "daisyui" {
exclude: properties; exclude: properties;
themes: winter --default, night --prefersdark, cupcake, coffee; themes: winter --default, night --prefersdark, cupcake, coffee;
@@ -9,19 +8,19 @@
@tailwind utilities; @tailwind utilities;
@plugin 'tailwind-scrollbar' { @plugin 'tailwind-scrollbar' {
nocompatible: true; nocompatible: true;
} }
:root { :root {
--size-big: 4vw; --size-big: 4vw;
--size-medium: 3vw; --size-medium: 3vw;
--size-small: 2vw; --size-small: 2vw;
} }
/* Smooth Modern Ultra-Sleek Scrollbars */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 4px;
height: 6px; height: 4px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@@ -29,10 +28,83 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: #9ca3af; background: rgba(156, 163, 175, 0.28);
border-radius: 0; border-radius: 9999px;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.55);
} }
::-webkit-scrollbar-button { ::-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() const locale = await getLocale()
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning className="overflow-x-hidden">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden max-w-full`}
suppressHydrationWarning suppressHydrationWarning
> >
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
@@ -74,17 +74,19 @@ export default async function RootLayout({
<ThemeProvider> <ThemeProvider>
<ClientThemeWrapper> <ClientThemeWrapper>
<ClientDataFetcher > <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 /> <Header />
<div className="grid grid-cols-12 w-full"> <main className="grid grid-cols-12 w-full flex-1">
<div className="hidden sm:block md:col-span-4 lg:col-span-3 sticky top-0 self-start h-fit"> <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 /> <AvatarBar />
</div> </aside>
<div className="col-span-12 sm:col-span-8 lg:col-span-9"> <section className="col-span-12 sm:col-span-8 md:col-span-8 lg:col-span-9 flex flex-col min-w-0">
<ActionBar /> <ActionBar />
{children} <div className="flex-1 p-2 sm:p-3 sm:pt-1">
</div> {children}
</div> </div>
</section>
</main>
<Footer /> <Footer />
</div> </div>
+11 -11
View File
@@ -1,11 +1,11 @@
"use client"; "use client";
import RelicsInfo from "@/components/relicsInfo"; import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() { export default function RelicsInfoPage() {
return ( return (
<div className="w-full"> <div className="w-full">
<RelicsInfo></RelicsInfo> <RelicsInfo></RelicsInfo>
</div> </div>
); );
} }
+11 -11
View File
@@ -1,11 +1,11 @@
"use client" "use client"
import ShowCaseInfo from "@/components/showcaseCard" import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() { export default function ShowcaseCard() {
return ( return (
<div> <div>
<ShowCaseInfo /> <ShowCaseInfo />
</div> </div>
) )
} }
+11 -11
View File
@@ -1,11 +1,11 @@
"use client"; "use client";
import SkillsInfo from "@/components/skillsInfo"; import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() { export default function SkillsInfoPage() {
return ( return (
<div className="w-full"> <div className="w-full">
<SkillsInfo></SkillsInfo> <SkillsInfo></SkillsInfo>
</div> </div>
); );
} }
+509 -444
View File
@@ -1,445 +1,510 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import ParseText from "../parseText"; import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName"; import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion"; import useModelStore from "@/stores/modelStore";
import useModelStore from "@/stores/modelStore"; import useUserDataStore from "@/stores/userDataStore";
import useUserDataStore from "@/stores/userDataStore"; import { ModalConfig, RelicStore, AvatarProfileStore } from "@/types";
import { ModalConfig, RelicStore } from "@/types"; import { toast } from "react-toastify";
import { toast } from "react-toastify"; import useGlobalStore from "@/stores/globalStore";
import useGlobalStore from "@/stores/globalStore"; import { connectToPS, syncDataToPS } from "@/helper";
import { connectToPS, syncDataToPS } from "@/helper"; import CopyImport from "../importBar/copy";
import CopyImport from "../importBar/copy"; import useCopyProfileStore from "@/stores/copyProfile";
import useCopyProfileStore from "@/stores/copyProfile"; import AvatarBar from "../avatarBar";
import AvatarBar from "../avatarBar"; import useCurrentDataStore from "@/stores/currentDataStore";
import useCurrentDataStore from "@/stores/currentDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
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() export default function ActionBar() {
const { avatarSelected } = useCurrentDataStore() const router = useRouter()
const { damageType, baseType } = useDetailDataStore() const pathname = usePathname()
const { setResetData } = useCopyProfileStore() const { avatarSelected } = useCurrentDataStore(
const transI18n = useTranslations("DataPage") useShallow((state) => ({ avatarSelected: state.avatarSelected }))
const { locale } = useLocaleStore() )
const { const { damageType, baseType } = useDetailDataStore(
isOpenCreateProfile, useShallow((state) => ({
setIsOpenCreateProfile, damageType: state.damageType,
isOpenCopy, baseType: state.baseType,
setIsOpenCopy, }))
isOpenAvatars, )
setIsOpenAvatars const { setResetData } = useCopyProfileStore(
} = useModelStore() useShallow((state) => ({ setResetData: state.setResetData }))
const { avatars, setAvatar } = useUserDataStore() )
const [profileName, setProfileName] = useState(""); const transI18n = useTranslations("DataPage")
const [formState, setFormState] = useState("EDIT"); const { locale } = useLocaleStore(
const [profileEdit, setProfileEdit] = useState(-1); useShallow((state) => ({ locale: state.locale }))
const { isConnectPS } = useGlobalStore() )
const {
const profileCurrent = useMemo(() => { isOpenCreateProfile,
if (!avatarSelected) return null; setIsOpenCreateProfile,
const avatar = avatars[avatarSelected.ID]; isOpenCopy,
return avatar?.profileList[avatar.profileSelect] || null; setIsOpenCopy,
}, [avatarSelected, avatars]); isOpenAvatars,
setIsOpenAvatars
const listProfile = useMemo(() => { } = useModelStore(
if (!avatarSelected) return []; useShallow((state) => ({
const avatar = avatars[avatarSelected.ID]; isOpenCreateProfile: state.isOpenCreateProfile,
return avatar?.profileList || []; setIsOpenCreateProfile: state.setIsOpenCreateProfile,
}, [avatarSelected, avatars]); isOpenCopy: state.isOpenCopy,
setIsOpenCopy: state.setIsOpenCopy,
isOpenAvatars: state.isOpenAvatars,
const handleUpdateProfile = () => { setIsOpenAvatars: state.setIsOpenAvatars,
if (!profileName.trim()) return; }))
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) { )
const newListProfile = [...listProfile] const { avatars, setAvatar } = useUserDataStore(
const newProfile = { useShallow((state) => ({
profile_name: profileName, avatars: state.avatars,
lightcone: null, setAvatar: state.setAvatar,
relics: {} as Record<string, RelicStore> }))
} )
newListProfile.push(newProfile) const [profileName, setProfileName] = useState("");
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 }) const [formState, setFormState] = useState("EDIT");
toast.success("Profile created successfully") const [profileEdit, setProfileEdit] = useState(-1);
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) { const { isConnectPS } = useGlobalStore(
const newListProfile = [...listProfile] useShallow((state) => ({ isConnectPS: state.isConnectPS }))
newListProfile[profileEdit].profile_name = profileName; )
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully") const profileCurrent = useMemo(() => {
} if (!avatarSelected) return null;
handleCloseModal("update_profile_modal"); const avatar = avatars[avatarSelected.ID];
}; return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
}, [avatarSelected, avatars]);
const handleShow = (modalId: string) => { const listProfile = useMemo(() => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; if (!avatarSelected) return [];
if (modal) { const avatar = avatars[avatarSelected.ID];
modal.showModal(); return avatar?.profileList || [];
} }, [avatarSelected, avatars]);
};
const handleUpdateProfile = () => {
// Close modal handler if (!profileName.trim()) return;
const handleCloseModal = (modalId: string) => { if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const newProfile: AvatarProfileStore = {
if (modal) { profile_name: profileName,
modal.close(); lightcone: null,
} relics: {} as Record<string, RelicStore>
}; };
const newListProfile = [...listProfile, newProfile];
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
const actionMove = (path: string) => { toast.success("Profile created successfully")
router.push(`/${path}`) } else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
} const newListProfile = listProfile.map((p, idx) =>
idx === profileEdit ? { ...p, profile_name: profileName } : p
const handleProfileSelect = (profileId: number) => { );
if (!avatarSelected) return; setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
if (avatars[avatarSelected.ID].profileSelect === profileId) return; toast.success("Profile updated successfully")
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId }) }
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`) handleCloseModal("update_profile_modal");
} };
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => { const handleShow = (modalId: string) => {
e.stopPropagation() const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (!avatarSelected || profileId == 0) return; if (modal) {
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) { modal.showModal();
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`) // Close modal handler
} const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
} if (modal) {
modal.close();
const handleConnectOrSyncPS = async () => { }
if (isConnectPS) { };
const res = await syncDataToPS()
if (res.success) { const actionMove = (path: string) => {
toast.success(transI18n("syncSuccess")) router.push(`/${path}`)
} else { }
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
} const handleProfileSelect = (profileId: number) => {
} else { if (!avatarSelected || !avatars[avatarSelected.ID]) return;
const res = await connectToPS() if (avatars[avatarSelected.ID].profileSelect === profileId) return;
if (res.success) { setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
toast.success(transI18n("connectedSuccess")) const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
} else { if (selectedProfile) {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`) toast.success(`Profile changed to: ${selectedProfile.profile_name}`)
} }
} }
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
const modalConfigs: ModalConfig[] = [ e.stopPropagation()
{ if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
id: "update_profile_modal", const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"), if (!targetProfile) return;
isOpen: isOpenCreateProfile, if (window.confirm(`Are you sure you want to delete profile: ${targetProfile.profile_name}?`)) {
onClose: () => { const currentSelect = avatars[avatarSelected.ID].profileSelect;
setIsOpenCreateProfile(false) const newListProfile = listProfile.filter((_, idx) => idx !== profileId);
handleCloseModal("update_profile_modal") const nextSelect = currentSelect >= profileId ? Math.max(0, currentSelect - 1) : currentSelect;
}, setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: nextSelect })
content: ( toast.success(`Profile ${targetProfile.profile_name} deleted successfully`)
<div className="px-6 space-y-4"> }
<div className="form-control"> }
<label className="label">
<span className="label-text text-primary font-semibold text-lg"> const handleConnectOrSyncPS = async () => {
{transI18n("profileName")} if (isConnectPS) {
</span> const res = await syncDataToPS()
</label> if (res.success) {
<input toast.success(transI18n("syncSuccess"))
type="text" } else {
placeholder={transI18n("placeholderProfileName")} toast.error(`${transI18n("syncFailed")}: ${res.message}`)
className="input input-warning mt-1 w-full" }
value={profileName} } else {
onChange={(e) => setProfileName(e.target.value)} const res = await connectToPS()
/> if (res.success) {
</div> toast.success(transI18n("connectedSuccess"))
<div className="modal-action"> } else {
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}> toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
{formState === "CREATE" ? transI18n("create") : transI18n("update")} }
</button> }
</div> }
</div>
) const modalConfigs: ModalConfig[] = [
}, {
{ id: "update_profile_modal",
id: "copy_profile_modal", title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
title: transI18n("copyProfiles").toUpperCase(), isOpen: isOpenCreateProfile,
isOpen: isOpenCopy, onClose: () => {
onClose: () => { setIsOpenCreateProfile(false)
setIsOpenCopy(false) handleCloseModal("update_profile_modal")
handleCloseModal("copy_profile_modal") },
}, content: (
content: <CopyImport /> <div className="space-y-4">
}, <div className="form-control">
{ <label className="label">
id: "avatars_modal", <span className="label-text font-semibold text-base-content/80">
title: transI18n("avatars").toUpperCase(), {transI18n("profileName")}
isOpen: isOpenAvatars, </span>
onClose: () => { </label>
setIsOpenAvatars(false) <input
handleCloseModal("avatars_modal") type="text"
}, placeholder={transI18n("placeholderProfileName")}
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} /> className="input input-bordered input-primary w-full"
} value={profileName}
] onChange={(e) => setProfileName(e.target.value)}
autoFocus
// Handle ESC key to close modal />
useEffect(() => { </div>
for (const item of modalConfigs) { <div className="flex justify-end gap-2 pt-2">
if (!item?.isOpen) { <button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
handleCloseModal(item?.id || "") Cancel
} </button>
} <button className="btn btn-primary" onClick={handleUpdateProfile}>
const handleEscKey = (event: KeyboardEvent) => { {formState === "CREATE" ? transI18n("create") : transI18n("update")}
if (event.key === 'Escape') { </button>
for (const item of modalConfigs) { </div>
handleCloseModal(item?.id || "") </div>
} )
} },
}; {
id: "copy_profile_modal",
window.addEventListener('keydown', handleEscKey); title: transI18n("copyProfiles"),
return () => window.removeEventListener('keydown', handleEscKey); isOpen: isOpenCopy,
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]); onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
return ( content: <CopyImport />
<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"> {
id: "avatars_modal",
<div className="flex flex-col justify-center w-full"> title: transI18n("avatars"),
<div className="flex flex-wrap items-center gap-2 "> isOpen: isOpenAvatars,
<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"> onClose: () => {
{avatarSelected && ( setIsOpenAvatars(false)
<div className="flex flex-wrap items-center justify-start h-full w-full"> handleCloseModal("avatars_modal")
<Image },
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`} content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
alt={'damage type'} }
unoptimized ]
crossOrigin="anonymous"
className="h-10 w-10 object-contain" // Handle ESC key to close modal
width={100} useEffect(() => {
height={100} for (const item of modalConfigs) {
/> if (!item?.isOpen) {
<p className="text-center font-bold text-lg"> handleCloseModal(item?.id || "")
{transI18n(avatarSelected.BaseType.toLowerCase())} }
</p> }
<div className="text-center font-bold text-lg">{" / "}</div> const handleEscKey = (event: KeyboardEvent) => {
<ParseText if (event.key === 'Escape') {
locale={locale} for (const item of modalConfigs) {
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()} handleCloseModal(item?.id || "")
className={"font-bold text-lg"} }
/> }
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div> };
</div>
)} window.addEventListener('keydown', handleEscKey);
</div> return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
</div>
const navTabs = [
<div className="flex flex-wrap items-center gap-2"> { label: transI18n("characterInformation"), path: "", icon: User },
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span> { label: transI18n("relics"), path: "relics-info", icon: Shield },
<div className="dropdown dropdown-center md:dropdown-start"> { label: transI18n("eidolons"), path: "eidolons-info", icon: Sparkles },
<div { label: transI18n("skills"), path: "skills-info", icon: Zap },
tabIndex={0} { label: transI18n("showcaseCard"), path: "showcase-card", icon: ImageIcon },
role="button" ];
className="btn btn-warning border-info btn-soft gap-1"
> const currentActivePath = pathname?.replace(/^\//, '') || "";
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name} return (
</span> <div className="w-full bg-transparent px-3 sm:px-4 py-2 space-y-2">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {/* Top Row: Character Identity + Profile Selector + Connect PS */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> <div className="flex flex-wrap items-center justify-between gap-2.5">
</svg> {/* Character Identity */}
</div> {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">
<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"> <Image
{listProfile.map((profile, index) => ( src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
<li key={index} className="grid grid-cols-12"> alt="damage type"
<button unoptimized
className={`col-span-8 btn btn-ghost`} crossOrigin="anonymous"
onClick={() => handleProfileSelect(index)} className="h-5 w-5 object-contain"
> width={20}
<span className="flex-1 truncate text-left"> height={20}
{profile.profile_name} />
{index === 0 && ( <span className="text-base-content/85 font-medium">{transI18n(avatarSelected.BaseType.toLowerCase())}</span>
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span> <span className="text-base-content/30">/</span>
)} <ParseText
</span> locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
</button> className="font-bold text-primary"
{index !== 0 && ( />
<> <span className="text-[11px] font-mono text-base-content/60 font-semibold">#{avatarSelected.ID}</span>
<button </div>
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"); {/* Profile Selector & Quick Actions */}
setProfileName(profile.profile_name) <div className="flex items-center h-8.5 rounded-xl bg-base-200/80 border border-base-content/10 p-0.5 shadow-xs">
setProfileEdit(index) <div className="dropdown dropdown-bottom dropdown-start h-full flex items-center">
setIsOpenCreateProfile(true) <div
handleShow("update_profile_modal") tabIndex={0}
}} role="button"
title="Edit Profile" 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"
> >
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span className="text-[11px] uppercase tracking-wider text-base-content/60 font-semibold">{transI18n("profile")}:</span>
<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" /> <span className="truncate max-w-28 sm:max-w-40 text-xs text-base-content font-bold">
</svg> {profileCurrent?.profile_name || "Default Profile"}
</button> </span>
<button <svg className="w-3.5 h-3.5 opacity-60 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
onClick={(e) => { </svg>
handleDeleteProfile(index, e) </div>
}}
title="Delete Profile" <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) => (
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24"> <li key={index} className="flex flex-row items-center justify-between rounded-xl px-1 hover:bg-base-200">
<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" /> <button
</svg> className="flex-1 text-left py-2 px-2 text-xs font-medium truncate"
</button> onClick={() => handleProfileSelect(index)}
</> >
)} <span className="truncate">{profile.profile_name}</span>
</li> {index === 0 && (
))} <span className="text-[10px] text-base-content/50 ml-1.5 font-mono">({transI18n("default")})</span>
)}
<li className="border-t border-base-300 mt-2 pt-2 z-10"> </button>
<button {index !== 0 && (
onClick={() => { <div className="flex items-center gap-1">
setIsOpenCopy(true) <button
setResetData(baseType, damageType) className="btn btn-ghost btn-xs btn-circle text-warning hover:bg-warning/20"
handleShow("copy_profile_modal") onClick={() => {
}} setFormState("EDIT");
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" setProfileName(profile.profile_name)
> setProfileEdit(index)
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> setIsOpenCreateProfile(true)
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> handleShow("update_profile_modal")
</svg> }}
{transI18n("copyProfiles")} title="Edit Profile"
</button> >
<button <Edit2 className="w-3 h-3" />
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" </button>
onClick={() => { <button
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
setIsOpenCreateProfile(true) onClick={(e) => handleDeleteProfile(index, e)}
setFormState("CREATE"); title="Delete Profile"
setProfileName("") >
handleShow("update_profile_modal") <Trash2 className="w-3 h-3" />
}} </button>
> </div>
<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" /> </li>
</svg> ))}
{transI18n("addNewProfile")}
</button> <li className="border-t border-base-300 pt-1 mt-1">
</li> <button
</ul> onClick={() => {
</div> setIsOpenCopy(true)
<div className=" grid grid-cols-2 w-full sm:hidden gap-2"> setResetData(baseType, damageType)
<button handleShow("copy_profile_modal")
onClick={() => { }}
setIsOpenAvatars(true) className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
handleShow("avatars_modal") >
}} <Copy className="w-3.5 h-3.5" />
className="col-span-1 btn btn-warning btn-sm w-full"> {transI18n("copyProfiles")}
{transI18n("avatars")} </button>
</button> </li>
<div className="col-span-1 dropdown dropdown-center w-full"> <li>
<label tabIndex={0} className="btn btn-info btn-sm w-full"> <button
{transI18n("actions")} onClick={() => {
</label> setIsOpenCreateProfile(true)
<ul setFormState("CREATE");
tabIndex={0} setProfileName("")
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" handleShow("update_profile_modal")
> }}
<li> className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
<button onClick={() => actionMove('')}> >
{transI18n("characterInformation")} <Plus className="w-3.5 h-3.5" />
</button> {transI18n("addNewProfile")}
</li> </button>
<li> </li>
<button onClick={() => actionMove('relics-info')}> </ul>
{transI18n("relics")} </div>
</button>
</li> {/* Quick Profile Actions */}
<li> <div className="flex items-center border-l border-base-content/10 pl-1 pr-0.5 gap-0.5 h-5">
<button onClick={() => actionMove('eidolons-info')}> <button
{transI18n("eidolons")} onClick={() => {
</button> setIsOpenCopy(true)
</li> setResetData(baseType, damageType)
<li> handleShow("copy_profile_modal")
<button onClick={() => actionMove('skills-info')}> }}
{transI18n("skills")} 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"
</button> title={transI18n("copyProfiles")}
</li> >
<li> <Copy className="w-3.5 h-3.5" />
<button onClick={() => actionMove('showcase-card')}> </button>
{transI18n("showcaseCard")} <button
</button> onClick={() => {
</li> setIsOpenCreateProfile(true)
<li> setFormState("CREATE");
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> setProfileName("")
{isConnectPS ? transI18n("sync") : transI18n("connectPs")} handleShow("update_profile_modal")
</button> }}
</li> 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"
</ul> title={transI18n("addNewProfile")}
</div> >
</div> <Plus className="w-3.5 h-3.5" />
</button>
</div> </div>
</div> </div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full"> {/* Desktop Connect PS Button */}
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}> <div className="hidden sm:flex items-center gap-2 ml-auto tooltip tooltip-bottom" data-tip={isConnectPS ? transI18n("sync") : transI18n("connectPs")}>
{transI18n("characterInformation")} <button
</button> onClick={handleConnectOrSyncPS}
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}> 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 ${
{transI18n("relics")} isConnectPS
</button> ? "btn-outline btn-accent"
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}> : "btn-primary shadow-sm"
{transI18n("eidolons")} }`}
</button> >
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}> <RefreshCw className={`w-3.5 h-3.5 ${isConnectPS ? "text-accent" : ""}`} />
{transI18n("skills")} <span className="text-xs">{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</button> </button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}> </div>
{transI18n("showcaseCard")} </div>
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> {/* Mobile-Only Action Row (< 640px): [Avatars Button] & [Actions Dropdown] */}
{isConnectPS ? transI18n("sync") : transI18n("connectPs")} <div className="grid grid-cols-2 sm:hidden gap-2 w-full pt-1">
</button> <button
</div> onClick={() => {
setIsOpenAvatars(true);
{modalConfigs.map(({ id, title, onClose, content }) => ( handleShow("avatars_modal");
<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"> className="btn btn-warning btn-sm w-full font-bold shadow-xs"
<div className="sticky top-0 z-10"> >
<motion.button {transI18n("avatars")}
whileHover={{ scale: 1.1, rotate: 90 }} </button>
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" <div className="dropdown dropdown-end w-full">
onClick={onClose} <label tabIndex={0} className="btn btn-info btn-sm w-full font-bold shadow-xs">
> {transI18n("actions")}
✕ </label>
</motion.button> <ul
</div> tabIndex={0}
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> 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"
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"> >
{title} {navTabs.map((tab) => {
</h3> const Icon = tab.icon;
</div> return (
{content} <li key={tab.path}>
</div> <button
</dialog> onClick={() => actionMove(tab.path)}
))} className="flex items-center gap-2 py-2 px-3 text-xs font-semibold"
>
<Icon className="w-4 h-4" />
</div> <span>{tab.label}</span>
</div> </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" "use client"
import Image from "next/image" import Image from "next/image"
import { useMemo } from "react" import { useMemo } from "react"
import CharacterCard from "../card/characterCard" import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore" import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore" import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore'; import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper" import { calcRarity, getNameChar } from "@/helper"
import { useShallow } from "zustand/react/shallow";
export default function AvatarBar({ onClose }: { onClose?: () => void }) { import { Search, X } from "lucide-react"
const {
avatarSearch, export default function AvatarBar({ onClose }: { onClose?: () => void }) {
mapAvatarElementActive, const {
mapAvatarPathActive, avatarSearch,
setAvatarSearch, mapAvatarElementActive,
setAvatarSelected, mapAvatarPathActive,
setMapAvatarElementActive, setAvatarSearch,
setMapAvatarPathActive, setAvatarSelected,
setSkillIDSelected, setMapAvatarElementActive,
} = useCurrentDataStore() setMapAvatarPathActive,
const { mapAvatar, baseType, damageType } = useDetailDataStore() setSkillIDSelected,
const transI18n = useTranslations("DataPage") } = useCurrentDataStore(
const {locale} = useLocaleStore() useShallow((state) => ({
avatarSearch: state.avatarSearch,
const listAvatar = useMemo(() => { mapAvatarElementActive: state.mapAvatarElementActive,
if (!mapAvatar || !locale || !transI18n) return [] mapAvatarPathActive: state.mapAvatarPathActive,
setAvatarSearch: state.setAvatarSearch,
let list = Object.values(mapAvatar) setAvatarSelected: state.setAvatarSelected,
setMapAvatarElementActive: state.setMapAvatarElementActive,
if (avatarSearch) { setMapAvatarPathActive: state.setMapAvatarPathActive,
list = list.filter(item => setSkillIDSelected: state.setSkillIDSelected,
getNameChar(locale, transI18n, item) }))
.toLowerCase() )
.includes(avatarSearch.toLowerCase()) const { mapAvatar, baseType, damageType } = useDetailDataStore(
) useShallow((state) => ({
} mapAvatar: state.mapAvatar,
baseType: state.baseType,
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v) damageType: state.damageType,
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v) }))
)
list = list.filter(item => const transI18n = useTranslations("DataPage")
(allElementFalse || mapAvatarElementActive[item.DamageType]) && const { locale } = useLocaleStore(
(allPathFalse || mapAvatarPathActive[item.BaseType]) useShallow((state) => ({ locale: state.locale }))
) )
list.sort((a, b) => { const listAvatar = useMemo(() => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity) if (!mapAvatar || !locale || !transI18n) return []
if (r !== 0) return r
return b.ID - a.ID let list = Object.values(mapAvatar)
})
if (avatarSearch) {
return list list = list.filter(item =>
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n]) getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
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"> const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
<div className="flex flex-col h-full gap-2"> const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
<div className="flex flex-col gap-2">
<div className="flex justify-center"> list = list.filter(item =>
<input type="text" (allElementFalse || mapAvatarElementActive[item.DamageType]) &&
placeholder={transI18n("placeholderCharacter")} (allPathFalse || mapAvatarPathActive[item.BaseType])
className="input input-bordered input-primary w-full" )
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)} list.sort((a, b) => {
/> const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
</div> if (r !== 0) return r
<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"> return b.ID - a.ID
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => ( })
<div
key={key} return list
onClick={() => { }, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}} return (
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg" <div className="flex flex-col h-full gap-2 p-2">
style={{ {/* Compact Search Bar */}
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280" <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" />
<Image <input
src={`${process.env.CDN_URL}/${value.Icon}`} type="text"
alt={key} placeholder={transI18n("placeholderCharacter")}
unoptimized className="grow text-xs bg-transparent border-none outline-none focus:outline-none placeholder:text-base-content/50"
crossOrigin="anonymous" value={avatarSearch}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md" onChange={(e) => setAvatarSearch(e.target.value)}
width={200} />
height={200} /> {avatarSearch && (
</div> <button
))} type="button"
</div> onClick={() => setAvatarSearch("")}
className="text-base-content/50 hover:text-base-content"
<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]) => ( <X className="w-3.5 h-3.5" />
<div </button>
key={key} )}
onClick={() => { </label>
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}} {/* Compact Filter Section */}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer" <div className="p-1 rounded-xl bg-base-200/80 border border-base-content/15 space-y-1 shadow-xs">
style={{ {/* Element Filters - 7 Elements on 1 Row (gap-0.5) */}
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280" <div className="grid grid-cols-7 gap-0.5">
}} {Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
> const isActive = !!mapAvatarElementActive[key];
return (
<Image <button
src={`${process.env.CDN_URL}/${value.Icon}`} key={key}
unoptimized type="button"
crossOrigin="anonymous" onClick={() => {
alt={key} setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md" }}
width={200} className={`h-8.5 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
height={200} /> isActive
</div> ? "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"
}`}
</div> title={value.DamageTypeName || key}
</div> >
<Image
<div className="flex items-start h-full"> src={`${process.env.CDN_URL}/${value.Icon}`}
<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"> alt={key}
{listAvatar.map((item, index) => ( unoptimized
<div key={index} onClick={() => { crossOrigin="anonymous"
setAvatarSelected(item); className="w-6.5 h-6.5 object-contain drop-shadow-xs"
setSkillIDSelected(null) width={32}
if (onClose) onClose() height={32}
}}> />
<CharacterCard data={item} /> </button>
</div> );
))} })}
</ul> </div>
</div>
</div> {/* Path Filters - 9 Paths on 1 Row (gap-0.5) */}
<div className="grid grid-cols-9 gap-0.5">
</div> {Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
</div> const isActive = !!mapAvatarPathActive[key];
</div> 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"; "use client";
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types'; import { AvatarDetail } from '@/types';
import ParseText from '../parseText'; import ParseText from '../parseText';
import Image from 'next/image'; import Image from 'next/image';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore'; import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
interface CharacterCardProps {
data: AvatarDetail import useUserDataStore from '@/stores/userDataStore';
}
interface CharacterCardProps {
export default function CharacterCard({ data }: CharacterCardProps) { data: AvatarDetail
const { locale } = useLocaleStore(); }
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore() export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
return ( const transI18n = useTranslations("DataPage");
<li const { baseType, damageType } = useDetailDataStore(
className="z-10 flex flex-col items-center rounded-xl shadow-xl useShallow((state) => ({
bg-linear-to-br from-base-300 via-base-100 to-warning/70 baseType: state.baseType,
transform transition-transform duration-300 ease-in-out damageType: state.damageType,
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 const { avatars } = useUserDataStore(
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5" useShallow((state) => ({ avatars: state.avatars }))
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50" )
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`} const userAvatar = avatars[data.ID.toString()]
> const level = userAvatar?.level
const rank = userAvatar?.data?.rank ?? 0
<div className="relative w-full h-32 lg:h-26 xl:h-36">
<Image const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
width={376} const is4Star = data.Rarity === "CombatPowerAvatarRarityType4";
height={512}
unoptimized return (
crossOrigin="anonymous" <li
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`} 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"
priority={true} >
className="rounded-md w-full h-full object-contain" {/* Artwork container with vibrant rarity background */}
alt="ALT" <div
/> className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
<Image is5Star
width={32} ? "from-yellow-400 via-amber-500/85 to-amber-700/70"
height={32} : is4Star
unoptimized ? "from-purple-400 via-purple-600/80 to-purple-800/60"
crossOrigin="anonymous" : "from-sky-400 via-blue-600/80 to-blue-800/60"
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={376}
<Image height={512}
width={32} unoptimized
height={32} crossOrigin="anonymous"
unoptimized src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
crossOrigin="anonymous" priority={true}
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`} className="w-full h-full object-contain transition-transform duration-300 ease-out group-hover:scale-105"
className="absolute top-0 right-0 w-6 h-6 rounded-full" alt={data.ID.toString()}
alt={data.BaseType.toLowerCase()} />
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF" {/* Level and Eidolons Badge Overlay */}
}} {level && (
/> <div className="absolute bottom-1 left-1 flex items-center gap-1 z-10">
</div> <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">
</div> {level}
</span>
<ParseText {rank > 0 && (
locale={locale} <span className="bg-amber-500/80 text-black text-[10px] font-black font-mono px-1 py-0.2 rounded shadow-xs">
text={getNameChar(locale, transI18n, data)} E{rank}
className=" </span>
w-full px-0.5 )}
my-1 </div>
text-center font-bold text-shadow-white )}
leading-tight
wrap-break-word {/* Damage Type Badge */}
text-sm sm:text-base 2xl:text-lg {damageType?.[data.DamageType]?.Icon && (
" <Image
/> width={28}
</li> 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"; "use client";
import React from 'react'; import React from 'react';
import { CharacterInfoCardType } from '@/types'; import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image'; import Image from 'next/image';
import ParseText from '../parseText'; import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore'; import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName'; 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); export default function CharacterInfoCard({
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(); character,
const { locale } = useLocaleStore(); selectedCharacters,
onCharacterToggle
return ( }: {
<div character: CharacterInfoCardType,
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected selectedCharacters: CharacterInfoCardType[],
? 'border-blue-400 ring-2 ring-blue-400/50' onCharacterToggle: (characterId: CharacterInfoCardType) => void
: 'border-base-300/50 hover:border-base-300 opacity-75' }) {
}`} const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(
onClick={() => onCharacterToggle(character)} useShallow((state) => ({
> mapAvatar: state.mapAvatar,
mapLightCone: state.mapLightCone,
{/* Character Portrait */} baseType: state.baseType,
<div className="relative mb-4"> damageType: state.damageType,
<div className="w-full h-48 rounded-lg overflow-hidden relative"> }))
<Image );
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`} const { locale } = useLocaleStore(
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)} useShallow((state) => ({ locale: state.locale }))
width={376} );
height={512}
unoptimized const avatar = mapAvatar?.[character.avatar_id.toString()];
crossOrigin="anonymous"
className="w-full h-full object-contain" return (
/> <div
<Image className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
width={48} isSelected
height={48} ? 'bg-primary/10 border-primary shadow-lg shadow-primary/20 ring-2 ring-primary/40'
unoptimized : 'bg-base-200/60 border-white/5 hover:border-white/20 opacity-80 hover:opacity-100 hover:bg-base-200'
crossOrigin="anonymous" }`}
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`} onClick={() => onCharacterToggle(character)}
className="absolute top-0 left-0 w-10 h-10 rounded-full" >
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()} {/* Character Portrait */}
/> <div className="relative mb-3 aspect-[4/5] rounded-xl overflow-hidden bg-base-300/40 flex items-center justify-center">
<Image {avatar && (
width={48} <>
height={48} <Image
unoptimized src={`${process.env.CDN_URL}/${avatar.Image?.AvatarIconPath}`}
crossOrigin="anonymous" alt={getLocaleName(locale, avatar.Name)}
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`} width={376}
className="absolute top-0 right-0 w-10 h-10 rounded-full" height={512}
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()} unoptimized
style={{ crossOrigin="anonymous"
boxShadow: "inset 0 0 8px 4px #9CA3AF" className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
}} />
/> {damageType?.[avatar.DamageType]?.Icon && (
</div> <div className="absolute top-1.5 left-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
</div> <Image
width={24}
{/* Character Name and Level */} height={24}
<div className="w-full rounded-lg flex items-center justify-center mb-2"> unoptimized
<div className="text-center"> crossOrigin="anonymous"
<ParseText className="text-lg font-bold" src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)} className="w-5 h-5 object-contain"
locale={locale} alt={avatar.DamageType.toLowerCase()}
/> />
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div> </div>
</div> )}
</div> {baseType?.[avatar.BaseType]?.Icon && (
{character.relics.length > 0 && ( <div className="absolute top-1.5 right-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
<div className="flex flex-wrap items-center justify-center gap-2 mb-4"> <Image
{character.relics.map((relic, index) => ( width={24}
<div key={index} className="relative"> height={24}
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50"> unoptimized
<Image 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`} src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
alt="Relic" className="w-5 h-5 object-contain"
unoptimized alt={avatar.BaseType.toLowerCase()}
crossOrigin="anonymous" />
width={124} </div>
height={124} )}
className="w-14 h-14 object-contain" </>
/> )}
</div> </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} {/* Character Name and Level Badge */}
</div> <div className="text-center mb-3">
</div> <ParseText
))} className="text-base font-bold line-clamp-1"
</div> text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
)} locale={locale}
/>
{/* Light Cone */} <div className="flex items-center justify-center gap-2 mt-1">
{character.lightcone.item_id && ( <span className="text-xs font-mono font-semibold px-2 py-0.5 rounded-full bg-base-300/80 text-base-content/80">
<div className=""> Lv.{character.level}
<div className="rounded-lg h-42 flex items-center justify-center"> </span>
<Image <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">
unoptimized E{character.rank}
crossOrigin="anonymous" </span>
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`} </div>
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)} </div>
width={348}
height={408} {/* Relics Mini-grid */}
className="w-full h-full object-contain rounded-lg" {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> <div key={index} className="relative group/relic">
<div className="w-full h-full rounded-lg flex items-center justify-center"> <div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
<div className="text-center"> <Image
<div className="text-lg font-bold"> src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
<ParseText alt="Relic"
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)} unoptimized
locale={locale} crossOrigin="anonymous"
/> width={32}
</div> height={32}
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div> className="w-6 h-6 object-contain"
</div> />
</div> </div>
</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>
)}
{/* 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"; "use client";
import { getLocaleName } from '@/helper'; import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import ParseText from '../parseText'; import { useShallow } from 'zustand/react/shallow';
import Image from 'next/image'; import ParseText from '../parseText';
import { LightConeDetail } from '@/types'; import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail interface LightconeCardProps {
} data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore(); const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
const text = getLocaleName(locale, data.Name) const text = getLocaleName(locale, data.Name);
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg const is5Star = data.Rarity === "CombatPowerLightconeRarity5";
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 const is4Star = data.Rarity === "CombatPowerLightconeRarity4";
hover:scale-105 cursor-pointer min-h-55"
> return (
<div <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">
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5" {/* Artwork container with vibrant rarity background */}
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50" <div
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" : className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
"from-blue-400 via-blue-600/70 to-blue-800/50" is5Star
}`} ? "from-yellow-400 via-amber-500/85 to-amber-700/70"
> : is4Star
? "from-purple-400 via-purple-600/80 to-purple-800/60"
<div className="relative w-full h-full"> : "from-sky-400 via-blue-600/80 to-blue-800/60"
<Image }`}
loading="lazy" >
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`} <Image
unoptimized loading="lazy"
crossOrigin="anonymous" src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
width={348} unoptimized
height={408} crossOrigin="anonymous"
className="w-full h-full rounded-md object-cover" width={348}
alt="ALT" height={408}
/> className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
</div> alt={data?.Name?.en || "Lightcone"}
</div> />
</div>
<ParseText
locale={locale} {/* Name Label - Uniform Fixed-Height Footer */}
text={text} <div className="w-full h-11 px-1 py-0.5 text-center flex items-center justify-center bg-base-100">
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight" <ParseText
/> locale={locale}
</li> 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"; "use client";
import React from 'react'; import React from 'react';
import { AvatarProfileCardType } from '@/types'; import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image'; import Image from 'next/image';
import ParseText from '../parseText'; import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore'; import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName'; 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 }) { export default function ProfileCard({
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key); profile,
const { mapLightCone } = useDetailDataStore(); selectedProfile,
const { locale } = useLocaleStore(); onProfileToggle
}: {
return ( profile: AvatarProfileCardType,
<div selectedProfile: AvatarProfileCardType[],
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected onProfileToggle: (profileId: AvatarProfileCardType) => void
? 'border-blue-400 ring-2 ring-blue-400/50' }) {
: 'border-base-300/50 hover:border-base-300 opacity-75' const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
}`} const { mapLightCone } = useDetailDataStore(
useShallow((state) => ({ mapLightCone: state.mapLightCone }))
onClick={() => onProfileToggle(profile)} );
> const { locale } = useLocaleStore(
{/* Light Cone */} useShallow((state) => ({ locale: state.locale }))
{profile.lightcone && ( );
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center"> return (
<Image <div
unoptimized className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
crossOrigin="anonymous" isSelected
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`} ? 'bg-primary/10 border-primary shadow-md ring-2 ring-primary/40'
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)} : 'bg-base-200/80 border-base-300 hover:border-primary/40 hover:bg-base-200'
width={348} }`}
height={408} onClick={() => onProfileToggle(profile)}
className="w-full h-full object-contain rounded-lg" >
/> {/* Light Cone */}
{profile.lightcone && (
</div> <div className="p-2 rounded-xl bg-base-100 border border-base-300 text-center mb-3">
<div className="w-full h-full rounded-lg flex items-center justify-center"> <div className="h-32 flex items-center justify-center mb-2">
<div className="text-center"> <Image
<div className="text-lg font-bold"> unoptimized
<ParseText crossOrigin="anonymous"
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)} src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
locale={locale} alt="Lightcone"
/> width={240}
</div> height={300}
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div> className="h-full w-auto object-contain rounded-lg transition-transform duration-200 group-hover:scale-105"
</div> />
</div> </div>
</div> <div className="text-sm font-bold truncate">
)} <ParseText
text={getLocaleName(locale, mapLightCone?.[profile.lightcone.item_id.toString()]?.Name)}
{Object.keys(profile.relics).length > 0 && ( locale={locale}
<div className="flex flex-wrap items-center justify-center gap-2 mb-4"> />
{Object.values(profile.relics).map((relic, index) => ( </div>
<div key={index} className="relative"> <div className="flex items-center justify-center gap-2 mt-1">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50"> <span className="text-xs font-mono font-medium text-base-content/70">Lv.{profile.lightcone.level}</span>
<Image <span className="text-xs font-mono font-bold text-primary">S{profile.lightcone.rank}</span>
unoptimized </div>
crossOrigin="anonymous" </div>
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} {/* Relics Mini-grid */}
height={124} {Object.keys(profile.relics).length > 0 && (
className="w-14 h-14 object-contain" <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> <div key={index} className="relative group/relic">
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded"> <div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
+{relic.level} <Image
</div> unoptimized
</div> 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`}
</div> alt="Relic"
)} width={32}
</div> 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"; "use client";
import useRelicMakerStore from "@/stores/relicMakerStore"; import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image"; import { useShallow } from "zustand/react/shallow";
import { useMemo } from "react"; import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string interface RelicCardProps {
avatarId: string 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'; const getRarityColor = (rarity: string) => {
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500'; switch (rarity) {
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':
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500'; 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';
default: return 'border-gray-500 shadow-gray-500/50'; 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';
const getRarityName = (slot: string) => { case '3':
switch (slot) { 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';
case '1': return ( default:
<div className="flex items-center gap-1"> return 'border-base-content/20 bg-base-300';
<Image }
unoptimized };
crossOrigin="anonymous"
src="/relics/HEAD.png" const getSlotIconAndName = (slot: string) => {
alt="Head" const slotMap: Record<string, { icon: string; name: string }> = {
width={20} '1': { icon: '/relics/HEAD.png', name: 'Head' },
height={20} '2': { icon: '/relics/HAND.png', name: 'Hands' },
className="bg-black/50 rounded-full" '3': { icon: '/relics/BODY.png', name: 'Body' },
/> '4': { icon: '/relics/FOOT.png', name: 'Feet' },
<h2>Head</h2> '5': { icon: '/relics/NECK.png', name: 'Planar Sphere' },
</div> '6': { icon: '/relics/OBJECT.png', name: 'Link Rope' },
); };
case '2': return (
<div className="flex items-center gap-1"> const current = slotMap[slot];
<Image if (!current) return null;
unoptimized
crossOrigin="anonymous" return (
src="/relics/HAND.png" <div className="flex items-center gap-1.5 justify-center mt-2.5">
alt="Hand" <Image
width={20} unoptimized
height={20} crossOrigin="anonymous"
className="bg-black/50 rounded-full" src={current.icon}
/> alt={current.name}
<h2>Hands</h2> width={18}
</div> height={18}
); className="w-4 h-4 object-contain theme-stat-icon opacity-85"
case '3': return ( />
<div className="flex items-center gap-1"> <span className="text-xs font-bold text-base-content">{current.name}</span>
<Image </div>
unoptimized );
crossOrigin="anonymous" };
src="/relics/BODY.png"
alt="Body" export default function RelicCard({ slot, avatarId }: RelicCardProps) {
width={20} const { avatars } = useUserDataStore(
height={20} useShallow((state) => ({ avatars: state.avatars }))
className="bg-black/50 rounded-full" )
/> const { selectedRelicSlot } = useRelicMakerStore(
<h2>Body</h2> useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
</div> )
);
case '4': return ( const relicDetail = useMemo(() => {
<div className="flex items-center gap-1"> const avatar = avatars[avatarId];
<Image if (avatar) {
unoptimized const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
crossOrigin="anonymous" return profile?.relics?.[slot] || null;
src="/relics/FOOT.png" }
alt="Foot" return null;
width={20} }, [avatars, avatarId, slot]);
height={20}
className="bg-black/50 rounded-full" const isSelected = selectedRelicSlot === slot;
/>
<h2>Feet</h2> return (
</div> <div className="flex flex-col items-center select-none group cursor-pointer">
); {relicDetail ? (
case '5': return ( <div className="flex flex-col items-center">
<div className="flex items-center gap-1"> <div
<Image className={`
unoptimized relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-3
crossOrigin="anonymous" ${getRarityColor(relicDetail.relic_id.toString()[0])}
src="/relics/NECK.png" flex items-center justify-center
alt="Neck" transition-all duration-200
width={20} ${isSelected ? 'ring-2 ring-primary/70 shadow-md ring-offset-2 ring-offset-base-100' : 'hover:border-primary/60'}
height={20} `}
className="bg-black/50 rounded-full" >
/> <Image
<h2>Planar sphere</h2> unoptimized
</div> crossOrigin="anonymous"
); src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
case '6': return ( alt="Relic"
<div className="flex items-center gap-1"> width={124}
<Image height={124}
unoptimized className="w-14 h-14 sm:w-16 sm:h-16 object-contain drop-shadow-md"
crossOrigin="anonymous" />
src="/relics/OBJECT.png"
alt="Object" {/* Level Badge */}
width={20} <div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
height={20} <span className="text-xs font-mono font-bold text-primary">+{relicDetail.level}</span>
className="bg-black/50 rounded-full" </div>
/> </div>
<h2>Link rope</h2>
</div> {getSlotIconAndName(slot)}
); </div>
default: return ''; ) : (
} <div className="flex flex-col items-center">
}; <div
export default function RelicCard({ slot, avatarId }: RelicCardProps) { className={`
const { avatars } = useUserDataStore() relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-2 border-dashed border-base-content/20
const { selectedRelicSlot } = useRelicMakerStore() bg-base-200/60 flex items-center justify-center
transition-all duration-200 group-hover:border-primary/60
const relicDetail = useMemo(() => { ${isSelected ? 'ring-2 ring-primary/70 border-solid bg-primary/10 ring-offset-2 ring-offset-base-100' : ''}
const avatar = avatars[avatarId]; `}
if (avatar) { >
if (avatar.profileList[avatar.profileSelect].relics[slot]) { <div className="flex flex-col items-center justify-center gap-1 text-base-content/40 group-hover:text-primary transition-colors">
return avatar.profileList[avatar.profileSelect].relics[slot]; <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" />
return null; </svg>
} </div>
return null;
}, [avatars, avatarId, slot]); {/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
return ( <span className="text-xs font-mono font-bold text-base-content/40">+0</span>
<div> </div>
{relicDetail ? ( </div>
<div
className="flex flex-col items-center cursor-pointer "> {getSlotIconAndName(slot)}
<div </div>
className={` )}
relative w-24 h-24 rounded-full </div>
${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>
)
} }
+83 -68
View File
@@ -1,69 +1,84 @@
"use client"; "use client";
import Image from 'next/image'; import Image from 'next/image';
import { AvatarDetail } from '@/types'; import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore'; import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
import { X } from 'lucide-react';
interface SimpleAvatarCardProps {
data: AvatarDetail; interface SimpleAvatarCardProps {
isSelected?: boolean; data: AvatarDetail;
onClick?: () => void; isSelected?: boolean;
showRemoveHover?: boolean; onClick?: () => void;
} showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore() export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore(
return ( useShallow((state) => ({
<div baseType: state.baseType,
onClick={onClick} damageType: state.damageType,
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" const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`} return (
> <div
<Image onClick={onClick}
width={80} 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 ${
height={80} isSelected ? 'ring-2 ring-primary opacity-60 scale-95' : ''
unoptimized }`}
crossOrigin="anonymous" >
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`} <div
priority={true} className={`relative w-full h-full rounded-xl overflow-hidden flex items-center justify-center bg-linear-to-b ${
className="w-full h-full object-contain" is5Star
alt="Avatar" ? "from-yellow-400 via-amber-500/85 to-amber-700/70"
/> : "from-purple-400 via-purple-600/80 to-purple-800/60"
<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} <Image
height={20} width={80}
unoptimized height={80}
crossOrigin="anonymous" unoptimized
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`} crossOrigin="anonymous"
className="w-full h-full object-contain" src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
alt="Element" priority={true}
/> className="w-full h-full object-contain transition-transform duration-200 group-hover:scale-105"
</div> alt="Avatar"
<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 {damageType?.[data.DamageType]?.Icon && (
width={20} <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">
height={20} <Image
unoptimized width={20}
crossOrigin="anonymous" height={20}
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`} unoptimized
className="w-full h-full object-contain" crossOrigin="anonymous"
alt="Path" src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
/> className="w-full h-full object-contain"
</div> alt="Element"
/>
{showRemoveHover && ( </div>
<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"> {baseType?.[data.BaseType]?.Icon && (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <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">
</svg> <Image
</div> width={20}
)} height={20}
</div> 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 useLocaleStore from '@/stores/localeStore'; import { useShallow } from 'zustand/react/shallow';
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react'; import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() { export default function ChangelogBar() {
const { changelog } = useLocaleStore() const { changelog } = useLocaleStore(
useShallow((state) => ({ changelog: state.changelog }))
const getIcon = (type: string) => { )
switch (type) {
case 'feature': const getIcon = (type: string) => {
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />; switch (type) {
case 'fix': case 'feature':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />; return <Sparkles className="w-3.5 h-3.5" />;
case 'improvement': case 'fix':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />; return <Bug className="w-3.5 h-3.5" />;
default: case 'improvement':
return <Package className="w-3 h-3 md:w-4 md:h-4" />; 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': const getBadgeClass = (type: string) => {
return 'badge-success'; switch (type) {
case 'fix': case 'feature':
return 'badge-error'; return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30';
case 'improvement': case 'fix':
return 'badge-warning'; return 'bg-rose-500/15 text-rose-400 border-rose-500/30';
default: case 'improvement':
return 'badge-info'; 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': const getTypeLabel = (type: string) => {
return 'Feature'; switch (type) {
case 'fix': case 'feature':
return 'Fix'; return 'Feature';
case 'improvement': case 'fix':
return 'Improvement'; return 'Fix';
default: case 'improvement':
return 'Update'; return 'Improvement';
} default:
}; return 'Update';
}
return ( };
<div className="md:p-4">
{/* Alert */} return (
{/* <div className="alert alert-info mb-4 md:p-2 p-1"> <div className="space-y-4">
<AlertCircle className="w-6 h-6" /> <div className="space-y-3">
</div> */} {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">
{/* Timeline */} {/* Version Header */}
<div className="flex flex-col gap-4"> <div className="flex flex-wrap items-center justify-between gap-2 pb-2.5 border-b border-base-content/10">
{changelog.map((change, index) => ( <div className="flex items-center gap-2">
<div key={index} className="bg-base-100 shadow-sm"> <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)}`}>
<div className="flex flex-col gap-2"> {getIcon(change.type)}
{/* Version Header */} {getTypeLabel(change.type)}
<div className="flex flex-row items-center gap-2 md:gap-3"> </span>
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}> <h3 className="font-extrabold text-sm sm:text-base tracking-tight">
{getIcon(change.type)} Version {change.version}
{getTypeLabel(change.type)} </h3>
</div> </div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version} <div className="flex items-center gap-1.5 text-xs text-base-content/50 font-mono">
</h2> <Calendar className="w-3.5 h-3.5" />
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto"> <span>{change.date}</span>
<Calendar className="w-3 h-3 md:w-4 md:h-4" /> </div>
<span className="text-sm">{change.date}</span> </div>
</div>
</div> {/* Changes List */}
<ul className="space-y-2 pt-1 text-xs sm:text-sm text-base-content/85">
{/* Changes List */} {change.items.map((item, itemIndex) => (
<ul className="space-y-3"> <li key={itemIndex} className="flex items-start gap-2.5">
{change.items.map((item, itemIndex) => ( <div className="mt-1 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
<li key={itemIndex} className="flex items-start gap-3"> <span className="leading-relaxed">{item}</span>
<div className="mt-1"> </li>
<Check className="w-5 h-5 text-success" /> ))}
</div> </ul>
<span className="text-base-content">{item}</span> </div>
</li> ))}
))} </div>
</ul> </div>
</div> );
</div>
))}
</div>
</div>
);
} }
+55 -55
View File
@@ -1,56 +1,56 @@
"use client" "use client"
import { import {
useFetchASGroupData, useFetchASGroupData,
useFetchAvatarData, useFetchAvatarData,
useFetchChangelog, useFetchChangelog,
useFetchConfigData, useFetchConfigData,
useFetchLightconeData, useFetchLightconeData,
useFetchMOCGroupData, useFetchMOCGroupData,
useFetchMonsterData, useFetchMonsterData,
useFetchPeakGroupData, useFetchPeakGroupData,
useFetchPFGroupData, useFetchPFGroupData,
useFetchRelicSetData useFetchRelicSetData
} from "@/lib/hooks" } from "@/lib/hooks"
export default function ClientDataFetcher({ export default function ClientDataFetcher({
children children
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const q1 = useFetchConfigData() const q1 = useFetchConfigData()
const q2 = useFetchAvatarData() const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData() const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData() const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData() const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData() const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData() const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData() const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData() const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog() const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10] const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading) const loading = queries.some(q => q.isLoading)
const progress = const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100 (queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) { if (loading) {
return ( return (
<div className="flex h-screen flex-col items-center justify-center gap-4"> <div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div> <div className="text-lg font-semibold">Loading data...</div>
<progress <progress
className="progress progress-primary w-56" className="progress progress-primary w-56"
value={progress} value={progress}
max="100" max="100"
/> />
<div>{Math.floor(progress)}%</div> <div>{Math.floor(progress)}%</div>
</div> </div>
) )
} }
return <>{children}</> return <>{children}</>
} }
+202 -168
View File
@@ -1,169 +1,203 @@
"use client" "use client"
import { connectToPS, syncDataToPS } from "@/helper" import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore" import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore" import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types" import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useState } from "react" import { useState } from "react"
import { useShallow } from "zustand/react/shallow"
export default function ConnectBar() { import { Server, RefreshCw, AlertCircle } from "lucide-react"
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' }); export default function ConnectBar() {
const { const transI18n = useTranslations("DataPage")
connectionType, const [message, setMessage] = useState({ text: '', type: '' });
privateType, const {
serverUrl, connectionType,
username, privateType,
password, serverUrl,
setConnectionType, username,
setPrivateType, password,
setServerUrl, setConnectionType,
setUsername, setPrivateType,
setPassword setServerUrl,
} = useConnectStore() setUsername,
const { isConnectPS, setIsConnectPS } = useGlobalStore() setPassword
} = useConnectStore(
return ( useShallow((state) => ({
<div className="px-6 py-4"> connectionType: state.connectionType,
<div className="form-control grid grid-cols-1 w-full mb-6"> privateType: state.privateType,
<label className="label"> serverUrl: state.serverUrl,
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span> username: state.username,
</label> password: state.password,
<select setConnectionType: state.setConnectionType,
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" setPrivateType: state.setPrivateType,
value={connectionType} setServerUrl: state.setServerUrl,
onChange={(e) => { setUsername: state.setUsername,
setIsConnectPS(false) setPassword: state.setPassword,
setConnectionType(e.target.value) }))
}} )
> const { isConnectPS, setIsConnectPS } = useGlobalStore(
<option value={PSConnectType.FireflyGo}>FireflyGo</option> useShallow((state) => ({
<option value={PSConnectType.RobinSR}>RobinSR</option> isConnectPS: state.isConnectPS,
<option value={PSConnectType.Other}>{transI18n("other")}</option> setIsConnectPS: state.setIsConnectPS,
</select> }))
</div> )
{connectionType === PSConnectType.Other && ( return (
<div className="flex flex-col md:space-x-4 mb-6 gap-2"> <div className="space-y-4">
<div className="form-control w-full mb-4 md:mb-0"> <div className="p-4 sm:p-6 rounded-2xl bg-base-200/60 border border-white/10 space-y-4">
<label className="label"> {/* Connection Type Selection */}
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span> <div className="space-y-1.5">
</label> <label className="block text-xs font-bold uppercase tracking-wider text-base-content/70">
<input {transI18n("connectionType")}
type="text" </label>
placeholder={transI18n("placeholderServerUrl")} <select
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" className="select select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-sm font-medium"
value={serverUrl} value={connectionType}
onChange={(e) => setServerUrl(e.target.value)} onChange={(e) => {
/> setIsConnectPS(false)
</div> setConnectionType(e.target.value)
<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> <option value={PSConnectType.FireflyGo}>FireflyGo</option>
</label> <option value={PSConnectType.RobinSR}>RobinSR</option>
<select <option value={PSConnectType.Other}>{transI18n("other")}</option>
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" </select>
value={privateType} </div>
onChange={(e) => setPrivateType(e.target.value)}
> {connectionType === PSConnectType.Other && (
<option value="Local">{transI18n("local")}</option> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
<option value="Server">{transI18n("server")}</option> <div className="space-y-1">
</select> <label className="block text-xs font-semibold text-base-content/70">
</div> {transI18n("serverUrl")}
</label>
<div className="form-control w-full mb-4 md:mb-0"> <input
<label className="label"> type="text"
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span> placeholder={transI18n("placeholderServerUrl")}
</label> 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"
<input value={serverUrl}
type="text" onChange={(e) => setServerUrl(e.target.value)}
placeholder={transI18n("placeholderUsername")} />
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" </div>
value={username}
onChange={(e) => setUsername(e.target.value)} <div className="space-y-1">
/> <label className="block text-xs font-semibold text-base-content/70">
</div> {transI18n("privateType")}
<div className="form-control w-full mb-4 md:mb-0"> </label>
<label className="label"> <select
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span> 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"
</label> value={privateType}
<input onChange={(e) => setPrivateType(e.target.value)}
type="password" >
placeholder={transI18n("placeholderPassword")} <option value="Local">{transI18n("local")}</option>
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" <option value="Server">{transI18n("server")}</option>
value={password} </select>
onChange={(e) => setPassword(e.target.value)} </div>
/>
</div> <div className="space-y-1">
</div> <label className="block text-xs font-semibold text-base-content/70">
)} {transI18n("username")}
</label>
{message.text && ( <input
<div className={`alert ${message.type === 'success' ? 'alert-success' : type="text"
message.type === 'error' ? 'alert-error' : 'alert-info' placeholder={transI18n("placeholderUsername")}
} mb-6`}> 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"
<span>{message.text}</span> value={username}
</div> onChange={(e) => setUsername(e.target.value)}
)} />
</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"> <div className="space-y-1">
<span className="text-md mr-2">{transI18n("status")}:</span> <label className="block text-xs font-semibold text-base-content/70">
<span {transI18n("password")}
className={`badge ${isConnectPS ? "badge-success" : "badge-error" </label>
} badge-lg`} <input
> type="password"
{isConnectPS ? transI18n("connected") : transI18n("unconnected")} placeholder={transI18n("placeholderPassword")}
</span> 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"
</div> value={password}
onChange={(e) => setPassword(e.target.value)}
{/* Buttons */} />
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end"> </div>
<button </div>
onClick={async () => { )}
const response = await connectToPS();
if (response.success) { {/* Status Message Notification */}
setMessage({ {message.text && (
type: "success", <div className={`p-3 rounded-xl border text-xs flex items-center gap-2 ${
text: transI18n("connectedSuccess"), message.type === 'success'
}); ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
} else { : message.type === 'error'
setMessage({ ? 'bg-rose-500/10 border-rose-500/30 text-rose-400'
type: "error", : 'bg-info/10 border-info/30 text-info'
text: response.message, }`}>
}); <AlertCircle className="w-4 h-4 shrink-0" />
} <span>{message.text}</span>
}} </div>
className="btn btn-primary w-full sm:w-auto" )}
>
{transI18n("connectPs")} {/* Footer Controls */}
</button> <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">
{isConnectPS && ( <span className="text-xs text-base-content/60">{transI18n("status")}:</span>
<button <div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-mono font-bold border ${
onClick={async () => { isConnectPS
const response = await syncDataToPS(); ? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
if (response.success) { : "bg-rose-500/15 border-rose-500/30 text-rose-400"
setMessage({ }`}>
type: "success", <span className={`w-2 h-2 rounded-full ${isConnectPS ? 'bg-emerald-400 animate-pulse' : 'bg-rose-400'}`}></span>
text: transI18n("syncSuccess"), <span>{isConnectPS ? transI18n("connected") : transI18n("unconnected")}</span>
}); </div>
} else { </div>
setMessage({
type: "error", <div className="flex items-center gap-2 w-full sm:w-auto">
text: `${transI18n("syncFailed")}: ${response.message}`, <button
}); onClick={async () => {
} const response = await connectToPS();
}} if (response.success) {
className="btn btn-success w-full sm:w-auto" setMessage({
> type: "success",
{transI18n("sync")} text: transI18n("connectedSuccess"),
</button> });
)} } else {
</div> setMessage({
</div> type: "error",
text: response.message,
</div> });
) }
}}
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" "use client"
import { replaceByParam, getLocaleName } from '@/helper'; import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image"; import Image from "next/image";
import ParseText from "../parseText"; import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore"; import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo'; import ExtraEffectList from '../extraInfo';
import { useShallow } from "zustand/react/shallow";
export default function EidolonsInfo() { export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore() const { avatarSelected } = useCurrentDataStore(
const { locale } = useLocaleStore() useShallow((state) => ({ avatarSelected: state.avatarSelected }))
const transI18n = useTranslations("DataPage") )
const { setAvatars, avatars } = useUserDataStore() const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
const charRank = useMemo(() => { )
if (!avatarSelected) return null; const transI18n = useTranslations("DataPage")
const avatar = avatars[avatarSelected.ID]; const { setAvatars, avatars } = useUserDataStore(
if (avatar?.enhanced != "") { useShallow((state) => ({
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks setAvatars: state.setAvatars,
} avatars: state.avatars,
return avatarSelected?.Ranks }))
}, [avatarSelected, avatars]); )
return ( const charRank = useMemo(() => {
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> if (!avatarSelected) return null;
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content"> const avatar = avatars[avatarSelected.ID];
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> if (avatar?.enhanced != "") {
{transI18n("eidolons")} return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
</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"> return avatarSelected?.Ranks
{charRank && avatars[avatarSelected?.ID || ""] && ( }, [avatarSelected, avatars]);
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(charRank || {}).map(([key, rank]) => ( const currentRank = avatars[avatarSelected?.ID.toString() || ""]?.data?.rank || 0;
<div key={key}
className="flex flex-col items-center" const handleToggleRank = (rankNum: number) => {
if (!avatarSelected) return;
> let newRank = rankNum;
<div if (currentRank === rankNum) {
className="cursor-pointer" newRank = rankNum - 1;
onClick={() => { }
let newRank = Number(rank.Rank) setAvatars({
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) { ...avatars,
newRank = Number(rank.Rank) - 1 [avatarSelected.ID]: {
} ...avatars[avatarSelected.ID],
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } }) 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 return (
crossOrigin="anonymous" <div className="w-full">
width={240} <div className="bg-base-100 rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-6">
height={240} {/* 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="text-lg pb-1 flex items-center justify-items-center gap-2"> <div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="inline-block text-indigo-500">{rank.Rank}.</span> <span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
<ParseText {transI18n("eidolons")}
locale={locale} </span>
text={getLocaleName(locale, rank.Name)} </h2>
className="text-center text-base font-normal leading-tight"
/> <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">
</div> <span className="text-base-content/60">Active:</span>
</div> <span className="text-primary text-sm font-extrabold">E{currentRank}</span>
</div>
<div className="text-sm font-normal"> </div>
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
<ExtraEffectList extras={rank?.Extra} locale={locale} /> {/* 6 Eidolons Organic Grid */}
</div> {charRank && avatars[avatarSelected?.ID || ""] && (
</div> <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);
</div> const isUnlocked = rankNum <= currentRank;
)} return (
</div> <div
</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" "use client"
import { useState } from "react" import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper" import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types" import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
type Props = { type Props = {
extras: Record<string, ExtraEffect> | undefined extras: Record<string, ExtraEffect> | undefined
locale: string locale: string
} }
export default function ExtraEffectList({ extras, locale }: Props) { export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false) const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null) const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage") const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null if (!extras || Object.keys(extras).length === 0) return null
return ( return (
<div className="mt-3"> <div className="mt-3">
<div <div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md" className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)} onClick={() => setOpenList(!openList)}
> >
<span className="text-sm font-semibold text-primary"> <span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length}) {transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span> </span>
<span <span
className={`transition-transform ${ className={`transition-transform ${
openList ? "rotate-90" : "" openList ? "rotate-90" : ""
}`} }`}
> >
▶ ▶
</span> </span>
</div> </div>
<div <div
className={`overflow-hidden transition-all duration-300 ${ className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0" openList ? "max-h-125 mt-2" : "max-h-0"
}`} }`}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => { {Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID const isOpen = openId === extra.ID
return ( return (
<div <div
key={extra.ID} key={extra.ID}
className="bg-primary/5 rounded-md" className="bg-primary/5 rounded-md"
> >
<div <div
className="flex items-center justify-between px-3 py-2 cursor-pointer" className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() => onClick={() =>
setOpenId(isOpen ? null : extra.ID) setOpenId(isOpen ? null : extra.ID)
} }
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded"> <span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")} {transI18n("extra")}
</span> </span>
<span className="text-sm font-medium text-primary/80"> <span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)} {getLocaleName(locale, extra.Name)}
</span> </span>
</div> </div>
<span <span
className={`transition-transform ${ className={`transition-transform ${
isOpen ? "rotate-90" : "" isOpen ? "rotate-90" : ""
}`} }`}
> >
▶ ▶
</span> </span>
</div> </div>
<div <div
className={`overflow-hidden transition-all duration-300 ${ className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0" isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`} }`}
> >
<div <div
className="text-sm opacity-90" className="text-sm opacity-90"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, extra.Desc), getLocaleName(locale, extra.Desc),
extra.Param extra.Param
) )
}} }}
/> />
</div> </div>
</div> </div>
) )
})} })}
</div> </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() { export default function Footer() {
return ( return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10"> <footer className="w-full border-t border-base-content/10 bg-base-100 py-6 px-4 mt-auto">
<aside> <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">
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p> <div className="flex items-center gap-2">
</aside> <span className="font-semibold text-base-content/80">Firefly Shelter</span>
</footer> <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" "use client"
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile"; import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard"; import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types"; import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image"; import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper"; import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage"; import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore"; import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore(); export default function CopyImport() {
const { avatarSelected } = useCurrentDataStore() const { avatars, setAvatar } = useUserDataStore(
const { mapAvatar, baseType, damageType } = useDetailDataStore() useShallow((state) => ({
const { locale } = useLocaleStore() avatars: state.avatars,
const { setAvatar: state.setAvatar,
selectedProfiles, }))
avatarCopySelected, );
setSelectedProfiles, const { avatarSelected } = useCurrentDataStore(
setAvatarCopySelected, useShallow((state) => ({
listElement, avatarSelected: state.avatarSelected,
listPath, }))
listRank, );
setListElement, const { mapAvatar, baseType, damageType } = useDetailDataStore(
setListPath, useShallow((state) => ({
setListRank mapAvatar: state.mapAvatar,
} = useCopyProfileStore() baseType: state.baseType,
const transI18n = useTranslations("DataPage") damageType: state.damageType,
}))
const [message, setMessage] = useState({ );
type: "", const { locale } = useLocaleStore(
text: "" useShallow((state) => ({
}) locale: state.locale,
}))
const listAvatar = useMemo(() => { );
if (!mapAvatar || !locale || !transI18n) return [] const {
let list = Object.values(mapAvatar); selectedProfiles,
const allElementFalse = !Object.values(listElement).some(v => v) avatarCopySelected,
const allPathFalse = !Object.values(listPath).some(v => v) setSelectedProfiles,
const allRarityFalse = !Object.values(listRank).some(v => v) setAvatarCopySelected,
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)])) listElement,
list.sort((a, b) => { listPath,
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity) listRank,
if (r !== 0) return r setListElement,
return a.ID - b.ID setListPath,
}) setListRank
return list } = useCopyProfileStore(
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n]) useShallow((state) => ({
selectedProfiles: state.selectedProfiles,
avatarCopySelected: state.avatarCopySelected,
const handleProfileToggle = (profile: AvatarProfileCardType) => { setSelectedProfiles: state.setSelectedProfiles,
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) { setAvatarCopySelected: state.setAvatarCopySelected,
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key)); listElement: state.listElement,
return; listPath: state.listPath,
} listRank: state.listRank,
setSelectedProfiles([...selectedProfiles, profile]); setListElement: state.setListElement,
}; setListPath: state.setListPath,
setListRank: state.setListRank,
}))
const clearSelection = () => { );
setSelectedProfiles([]); const transI18n = useTranslations("DataPage")
setMessage({
type: "success", const [message, setMessage] = useState({
text: transI18n("clearAll") 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 = () => { const selectAll = () => {
if (avatarCopySelected) { if (avatarCopySelected) {
const sourceAvatar = avatars[avatarCopySelected.ID.toString()] const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
@@ -79,32 +112,32 @@ export default function CopyImport() {
return null; return null;
} }
return { return {
key: index, key: index,
...profile, ...profile,
} as AvatarProfileCardType } as AvatarProfileCardType
}).filter((profile) => profile !== null)); }).filter((profile) => profile !== null));
} }
setMessage({ setMessage({
type: "success", type: "success",
text: transI18n("selectAll") text: transI18n("selectAll")
}) })
}; };
const handleCopy = () => { const handleCopy = () => {
if (selectedProfiles.length === 0) { if (selectedProfiles.length === 0) {
setMessage({ setMessage({
type: "error", type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile") text: transI18n("pleaseSelectAtLeastOneProfile")
}); });
return; return;
} }
if (!avatarCopySelected) { if (!avatarCopySelected) {
setMessage({ setMessage({
type: "error", type: "error",
text: transI18n("noAvatarToCopySelected") text: transI18n("noAvatarToCopySelected")
}); });
return; return;
} }
if (!avatarSelected) { if (!avatarSelected) {
setMessage({ setMessage({
type: "error", type: "error",
@@ -129,181 +162,205 @@ export default function CopyImport() {
if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) { if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) {
return null; return null;
} }
return { const cloned = structuredClone(profile);
...profile, cloned.profile_name = profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`;
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`, return cloned as AvatarProfileStore;
} as AvatarProfileStore }).filter((profile): profile is AvatarProfileStore => profile !== null);
}).filter((profile) => profile !== null);
const newAvatar = { const newAvatar = {
...targetAvatar, ...targetAvatar,
profileList: targetAvatar.profileList.concat(newListProfile), profileList: [...targetAvatar.profileList, ...newListProfile],
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1, profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
} }
setAvatar(newAvatar); setAvatar(newAvatar);
setSelectedProfiles([]); setSelectedProfiles([]);
setMessage({ setMessage({
type: "success", type: "success",
text: transI18n("copied") text: transI18n("copied")
}); });
}; };
return ( return (
<div className="rounded-lg px-2 min-h-[60vh]"> <div className="rounded-2xl p-2 sm:p-4 min-h-[50vh] space-y-4">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto space-y-4">
{/* Header */} {/* Header & Filters Card */}
<div className="mb-2"> <div className="bg-base-100 rounded-2xl p-4 border border-base-300 shadow-sm space-y-4">
{/* Filters Section */}
<div className="mt-2 w-full"> <div className="space-y-3">
<div className="text-xs font-bold uppercase tracking-wider text-base-content/60">{transI18n("filter")}</div>
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>{transI18n("filter")}</div> <div className="flex flex-wrap items-center gap-3 sm:gap-4">
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full"> {/* Path Filters */}
{/* Path */} <div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
<div> {Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
<div className="flex flex-wrap gap-2 justify-start items-center"> const isActive = !!listPath[key];
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => ( return (
<div <button
key={key} key={key}
onClick={() => { type="button"
setListPath({ ...listPath, [key]: !listPath[key] }) 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 ${
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" isActive
style={{ ? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
backgroundColor: listPath[key] ? "#374151" : "#6B7280" : "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
}}> }`}
<Image title={key}
unoptimized >
crossOrigin="anonymous" <Image
src={`${process.env.CDN_URL}/${value.Icon}`} unoptimized
alt={key} crossOrigin="anonymous"
className="h-8 w-8 object-contain rounded-md" src={`${process.env.CDN_URL}/${value.Icon}`}
width={200} alt={key}
height={200} className={`w-6 h-6 sm:w-7 sm:h-7 object-contain ${
/> isActive ? "theme-path-icon-active" : "theme-path-icon"
</div> }`}
))} width={32}
</div> height={32}
</div> />
</button>
{/* Element */} );
<div> })}
<div className="flex flex-wrap gap-2 justify-start items-center"> </div>
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div <div className="h-8 w-px bg-base-300 hidden md:block" />
key={key}
onClick={() => { {/* Element Filters */}
setListElement({ ...listElement, [key]: !listElement[key] }) <div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
}} {Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" const isActive = !!listElement[key];
style={{ return (
backgroundColor: listElement[key] ? "#374151" : "#6B7280" <button
}}> key={key}
<Image type="button"
unoptimized onClick={() => setListElement({ ...listElement, [key]: !listElement[key] })}
crossOrigin="anonymous" 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 ${
src={`${process.env.CDN_URL}/${value.Icon}`} isActive
alt={key} ? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md" : "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
width={200} }`}
height={200} title={key}
/> >
</div> <Image
))} unoptimized
</div> crossOrigin="anonymous"
</div> src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
{/* Rank */} className="w-6 h-6 sm:w-7 sm:h-7 object-contain drop-shadow-xs"
<div> width={32}
<div className="flex flex-wrap gap-2 justify-end items-center"> height={32}
{Object.entries(listRank).map(([key], index) => ( />
<div </button>
key={index} );
onClick={() => { })}
setListRank({ ...listRank, [key]: !listRank[key] }) </div>
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" <div className="h-8 w-px bg-base-300 hidden md:block" />
style={{
backgroundColor: listRank[key] ? "#374151" : "#6B7280" {/* Rank Filters */}
}}> <div className="flex items-center gap-1.5 sm:gap-2">
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div> {Object.entries(listRank).map(([key], index) => {
</div> const isActive = !!listRank[key];
))} return (
</div> <button
</div> key={index}
</div> type="button"
onClick={() => setListRank({ ...listRank, [key]: !listRank[key] })}
</div> 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 ${
<div className="grid grid-cols-1 gap-2"> isActive
? "bg-primary text-primary-content border-primary shadow-sm scale-105"
{listAvatar.length > 0 && ( : "bg-base-200 hover:bg-base-300 text-base-content/70 border-base-300"
<div> }`}
<div>{transI18n("characterName")}</div> >
<SelectCustomImage {key}★
customSet={listAvatar.map((avatar) => ({ </button>
value: avatar.ID.toString(), );
label: getNameChar(locale, transI18n, avatar), })}
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}` </div>
}))} </div>
excludeSet={[]} </div>
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select" {/* Character Select Dropdown */}
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)} {listAvatar.length > 0 && (
/> <div className="space-y-1.5 pt-2 border-t border-base-300/60">
</div> <div className="text-xs font-bold text-base-content/70">{transI18n("characterName")}</div>
)} <SelectCustomImage
customSet={listAvatar.map((avatar) => ({
</div> value: avatar.ID.toString(),
</div> label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
{/* Selection Info */} selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg"> placeholder="Character Select"
<span className="text-blue-400 font-medium"> setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
{transI18n("selectedProfiles")}: {selectedProfiles.length} />
</span> </div>
<button )}
onClick={clearSelection} </div>
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"
> {/* Selection Action Bar */}
{transI18n("clearAll")} <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">
</button> <div className="flex items-center gap-2">
<button <span className="text-xs sm:text-sm font-bold text-base-content">
onClick={selectAll} {transI18n("selectedProfiles")}:
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"> </span>
{transI18n("selectAll")} <span className="badge badge-primary font-mono font-bold text-xs">
</button> {selectedProfiles.length}
{selectedProfiles.length > 0 && ( </span>
<button </div>
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"> <div className="flex items-center gap-2">
{transI18n("copy")} <button
</button> onClick={clearSelection}
)} className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
</div> >
{message.type && message.text && ( {transI18n("clearAll")}
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div> </button>
)} <button
</div> onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
{/* Character Grid */} >
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> {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) => { {avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) { if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
return null; return null;
} }
return ( return (
<ProfileCard <ProfileCard
key={index} key={index}
profile={{ ...profile, key: index }} profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles} selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle} onProfileToggle={handleProfileToggle}
/> />
) )
})} })}
</div> </div>
</div> </div>
</div> </div>
) )
} }
+173 -136
View File
@@ -1,10 +1,10 @@
"use client" "use client"
import { useState } from "react"; import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard"; import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore"; import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api"; import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types"; import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper"; import { converterOneEnkaDataToAvatarStore } from "@/helper";
import useModelStore from "@/stores/modelStore"; import useModelStore from "@/stores/modelStore";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -12,6 +12,7 @@ import { useTranslations } from "next-intl";
import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod"; import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
import { z } from "zod"; import { z } from "zod";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>; 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), relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
})), })),
}); });
export default function EnkaImport() { export default function EnkaImport() {
const { const {
uidInput, uidInput,
setUidInput, setUidInput,
enkaData, enkaData,
selectedCharacters, selectedCharacters,
setSelectedCharacters, setSelectedCharacters,
setEnkaData, setEnkaData,
} = useEnkaStore(); } = useEnkaStore(
const transI18n = useTranslations("DataPage") useShallow((state) => ({
const { avatars, setAvatar } = useUserDataStore(); uidInput: state.uidInput,
const { setIsOpenImport } = useModelStore() setUidInput: state.setUidInput,
const { mapAvatar } = useDetailDataStore() 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 [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("") const [Error, setError] = useState("")
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? [] const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
const handlerFetchData = async () => { const handlerFetchData = async () => {
if (!uidInput) { if (!uidInput) {
setError(transI18n("pleaseEnterUid")) setError(transI18n("pleaseEnterUid"))
@@ -77,36 +100,36 @@ export default function EnkaImport() {
setIsLoading(false) setIsLoading(false)
} }
} }
const handleCharacterToggle = (character: CharacterInfoCardType) => { const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) { if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key)); setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return; return;
} }
setSelectedCharacters([...selectedCharacters, character]); setSelectedCharacters([...selectedCharacters, character]);
}; };
const clearSelection = () => { const clearSelection = () => {
setSelectedCharacters([]); setSelectedCharacters([]);
}; };
const selectAll = () => { const selectAll = () => {
if (enkaData) { if (enkaData) {
setSelectedCharacters(validCharacters.map(toCharacterInfoCard)); setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
} }
}; };
const handleImport = () => { const handleImport = () => {
if (selectedCharacters.length === 0) { if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter")); setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return; return;
} }
if (!enkaData) { if (!enkaData) {
setError(transI18n("noDataToImport")); setError(transI18n("noDataToImport"));
return; return;
} }
setError(""); setError("");
const listAvatars = { ...avatars } const listAvatars = { ...avatars }
const parsed = enkaResponseSchema.safeParse(enkaData) const parsed = enkaResponseSchema.safeParse(enkaData)
if (!parsed.success) { if (!parsed.success) {
setError(transI18n("failedToFetchEnkaData")); 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)) const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
filterData.forEach((character) => { filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatarId.toString()] } const currentAvatar = listAvatars[character.avatarId.toString()]
if (Object.keys(newAvatar).length !== 0) { if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
newAvatar.level = character.level ?? 0 const newProfileList = [...(currentAvatar.profileList || [])]
newAvatar.promotion = character.promotion ?? 0 const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length)
newAvatar.data = { if (newProfile) {
rank: character.rank ?? 0, newProfileList.push(newProfile)
skills: character.skillTreeList.reduce((acc, skill) => { }
acc[skill.pointId] = skill.level; const newAvatar = {
return acc; ...currentAvatar,
}, {} as Record<string, number>), level: character.level ?? 0,
} promotion: character.promotion ?? 0,
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length) data: {
if (newProfile) { rank: character.rank ?? 0,
newAvatar.profileList.push(newProfile) skills: character.skillTreeList.reduce((acc, skill) => {
newAvatar.profileSelect = newAvatar.profileList.length - 1 acc[skill.pointId] = skill.level;
} return acc;
setAvatar(newAvatar) }, {} as Record<string, number>),
} },
profileList: newProfileList,
}) profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
setIsOpenImport(false) }
toast.success(transI18n("importEnkaDataSuccess")) setAvatar(newAvatar)
}; }
})
return ( setIsOpenImport(false)
<div className="bg-slate-900 p-6"> toast.success(transI18n("importEnkaDataSuccess"))
<div className="max-w-7xl mx-auto"> };
{/* Header */}
<div className="mb-8"> return (
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1> <div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex gap-4 items-center mb-6"> {/* Header */}
<input <div className="space-y-4">
type="text" <h1 className="text-base-content text-xl sm:text-2xl font-bold">HSR UID</h1>
value={uidInput}
onChange={(e) => setUidInput(e.target.value)} <div className="flex flex-wrap gap-3 items-center">
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" <input
placeholder="Enter UID" type="text"
/> value={uidInput}
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors"> onChange={(e) => setUidInput(e.target.value)}
{transI18n("getData")} className="input input-bordered w-full max-w-xs rounded-xl bg-base-200 border-base-300 text-sm focus:border-primary"
</button> placeholder="Enter UID"
{selectedCharacters.length > 0 && ( />
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors"> <button onClick={handlerFetchData} className="btn btn-sm btn-success rounded-xl font-bold">
{transI18n("import")} {transI18n("getData")}
</button> </button>
)} {selectedCharacters.length > 0 && (
</div> <button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{Error && ( {transI18n("import")}
<div className="text-error text-base mt-2">😭 {Error}</div> </button>
)} )}
</div>
{/* Player Info */} {Error && (
{enkaData && ( <div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
<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> {/* Player Info */}
<div className="text-lg">Level: {enkaData.detailInfo.level}</div> {enkaData && (
</div> <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>
{/* Selection Info */} <div className="text-sm font-semibold">Level: <span className="font-bold">{enkaData.detailInfo.level}</span></div>
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg"> </div>
<span className="text-blue-400 font-medium"> )}
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span> {/* Selection Info */}
<button <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">
onClick={clearSelection} <div className="flex items-center gap-2">
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" <span className="text-xs sm:text-sm font-bold text-base-content">
> {transI18n("selectedCharacters")}:
{transI18n("clearAll")} </span>
</button> <span className="badge badge-primary font-mono font-bold text-xs">
<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"> {selectedCharacters.length}
{transI18n("selectAll")} </span>
</button> </div>
</div> <div className="flex items-center gap-2">
</div> <button
{isLoading && ( onClick={clearSelection}
<div className="flex justify-center items-center h-64"> className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div> >
</div> {transI18n("clearAll")}
)} </button>
{/* Character Grid */} <button
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> 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) => ( {validCharacters.map((character) => (
<CharacterInfoCard <CharacterInfoCard
key={character.avatarId} key={character.avatarId}
@@ -206,9 +243,9 @@ export default function EnkaImport() {
selectedCharacters={selectedCharacters} selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle} onCharacterToggle={handleCharacterToggle}
/> />
))} ))}
</div> </div>
</div> </div>
</div> </div>
); );
} }
+172 -138
View File
@@ -1,16 +1,17 @@
"use client" "use client"
import useFreeSRStore from "@/stores/freesrStore"; import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore"; import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types"; import { CharacterInfoCardType } from "@/types";
import { useState } from "react"; import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard"; import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod"; import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper"; import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
import { z } from "zod"; import { z } from "zod";
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>; type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
@@ -36,55 +37,75 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
})), })),
} }
} }
export default function FreeSRImport() { export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore(); const { avatars, setAvatar } = useUserDataStore(
const { mapAvatar } = useDetailDataStore(); useShallow((state) => ({
const { setIsOpenImport } = useModelStore() avatars: state.avatars,
const [isLoading, setIsLoading] = useState(false) 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 [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore() 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 transI18n = useTranslations("DataPage")
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData) const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
const handleCharacterToggle = (character: CharacterInfoCardType) => { const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) { if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key)); setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return; return;
} }
setSelectedCharacters([...selectedCharacters, character]); setSelectedCharacters([...selectedCharacters, character]);
}; };
const clearSelection = () => { const clearSelection = () => {
setSelectedCharacters([]); setSelectedCharacters([]);
}; };
const selectAll = () => { const selectAll = () => {
const parsed = freeSrJsonSchema.safeParse(freeSRData) const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (parsed.success) { if (parsed.success) {
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character))); setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
} }
}; };
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => { const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true) setIsLoading(true)
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) { if (!file) {
setSelectedCharacters([]) setSelectedCharacters([])
setFreeSRData(null) setFreeSRData(null)
setError(transI18n("pleaseSelectAFile")) setError(transI18n("pleaseSelectAFile"))
setIsLoading(false) setIsLoading(false)
return return
} }
if (!file.name.endsWith(".json") || file.type !== "application/json") { if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([]) setSelectedCharacters([])
setFreeSRData(null) setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile")) setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false) setIsLoading(false)
return return
} }
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
@@ -121,18 +142,18 @@ export default function FreeSRImport() {
reader.readAsText(file); reader.readAsText(file);
} }
}; };
const handleImport = () => { const handleImport = () => {
if (selectedCharacters.length === 0) { if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter")); setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return; return;
} }
if (!freeSRData) { if (!freeSRData) {
setError(transI18n("noDataToImport")); setError(transI18n("noDataToImport"));
return; return;
} }
setError(""); setError("");
const parsed = freeSrJsonSchema.safeParse(freeSRData) const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (!parsed.success) { if (!parsed.success) {
setError(transI18n("fileMustBeAValidJsonFile")); setError(transI18n("fileMustBeAValidJsonFile"));
@@ -142,75 +163,88 @@ export default function FreeSRImport() {
const listAvatars = { ...avatars } const listAvatars = { ...avatars }
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id)) const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => { filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] } const currentAvatar = listAvatars[character.avatar_id]
if (Object.keys(newAvatar).length !== 0) { if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
newAvatar.level = character.level const newProfileList = [...(currentAvatar.profileList || [])]
newAvatar.promotion = character.promotion const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id)
newAvatar.data = { if (newProfile) {
rank: character.data.rank ?? 0, newProfileList.push(newProfile)
skills: character.data.skills }
} const newAvatar = {
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id) ...currentAvatar,
if (newProfile) { level: character.level,
newAvatar.profileList.push(newProfile) promotion: character.promotion,
newAvatar.profileSelect = newAvatar.profileList.length - 1 data: {
} rank: character.data.rank ?? 0,
setAvatar(newAvatar) skills: character.data.skills
} },
profileList: newProfileList,
}) profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
setIsOpenImport(false) }
toast.success(transI18n("importFreeSRDataSuccess")) setAvatar(newAvatar)
} }
})
return ( setIsOpenImport(false)
<div className="bg-slate-900 p-6"> toast.success(transI18n("importFreeSRDataSuccess"))
<div className="max-w-7xl mx-auto"> }
{/* Header */}
<div className="mb-8"> return (
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1> <div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex gap-4 items-center mb-6"> {/* Header */}
<fieldset className="fieldset"> <div className="space-y-4">
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend> <h1 className="text-base-content text-xl sm:text-2xl font-bold">{transI18n("freeSRImport")}</h1>
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label> <div className="flex flex-wrap gap-4 items-center">
</fieldset> <fieldset className="fieldset">
<legend className="fieldset-legend text-xs font-semibold">{transI18n("pickAFile")}</legend>
{selectedCharacters.length > 0 && ( <input type="file" accept=".json" className="file-input file-input-sm file-input-bordered rounded-xl" onChange={handlerReadFile} />
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors"> <label className="label text-[11px] text-base-content/60">{transI18n("onlySupportFreeSRJsonFile")}</label>
{transI18n("import")} </fieldset>
</button>
)} {selectedCharacters.length > 0 && (
</div> <button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{Error && ( {transI18n("import")}
<div className="text-error text-base mt-2">😭 {Error}</div> </button>
)} )}
</div>
{Error && (
{/* Selection Info */} <div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
<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} {/* Selection Info */}
</span> <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">
<button <div className="flex items-center gap-2">
onClick={clearSelection} <span className="text-xs sm:text-sm font-bold text-base-content">
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("selectedCharacters")}:
> </span>
{transI18n("clearAll")} <span className="badge badge-primary font-mono font-bold text-xs">
</button> {selectedCharacters.length}
<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"> </span>
{transI18n("selectAll")} </div>
</button> <div className="flex items-center gap-2">
</div> <button
</div> onClick={clearSelection}
{isLoading && ( className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
<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> {transI18n("clearAll")}
</div> </button>
)} <button
{/* Character Grid */} onClick={selectAll}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> 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) => { {validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
return ( return (
<CharacterInfoCard <CharacterInfoCard
@@ -219,10 +253,10 @@ export default function FreeSRImport() {
selectedCharacters={selectedCharacters} selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle} onCharacterToggle={handleCharacterToggle}
/> />
) )
})} })}
</div> </div>
</div> </div>
</div> </div>
) )
} }
+211 -144
View File
@@ -1,145 +1,212 @@
"use client" "use client"
import { useMemo } from "react" import { useMemo } from "react"
import Image from "next/image"; import Image from "next/image";
import useLocaleStore from "@/stores/localeStore" import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard"; import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore"; import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore"; import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper"; import { calcRarity, getLocaleName } from "@/helper";
import { useShallow } from "zustand/react/shallow";
export default function LightconeBar() { import { Search, X } from "lucide-react";
const { locale } = useLocaleStore()
const { export default function LightconeBar() {
avatarSelected, const { locale } = useLocaleStore(
mapLightconePathActive, useShallow((state) => ({ locale: state.locale }))
mapLightconeRankActive, )
setMapLightconePathActive, const {
setMapLightconeRankActive, avatarSelected,
lightconeSearch, mapLightconePathActive,
setLightconeSearch mapLightconeRankActive,
} = useCurrentDataStore() setMapLightconePathActive,
const { setAvatar, avatars } = useUserDataStore() setMapLightconeRankActive,
const { setIsOpenLightcone } = useModelStore() lightconeSearch,
const { mapLightCone, baseType } = useDetailDataStore() setLightconeSearch
const transI18n = useTranslations("DataPage") } = useCurrentDataStore(
useShallow((state) => ({
const listLightcone = useMemo(() => { avatarSelected: state.avatarSelected,
if (!mapLightCone || !locale) return [] mapLightconePathActive: state.mapLightconePathActive,
mapLightconeRankActive: state.mapLightconeRankActive,
let list = Object.values(mapLightCone) setMapLightconePathActive: state.setMapLightconePathActive,
setMapLightconeRankActive: state.setMapLightconeRankActive,
if (lightconeSearch) { lightconeSearch: state.lightconeSearch,
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase())) setLightconeSearch: state.setLightconeSearch,
} }))
)
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v) const { setAvatar, avatars } = useUserDataStore(
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v) useShallow((state) => ({
setAvatar: state.setAvatar,
list = list.filter(item => avatars: state.avatars,
(allRankFalse || mapLightconeRankActive[item.Rarity]) && }))
(allPathFalse || mapLightconePathActive[item.BaseType]) )
) const { setIsOpenLightcone } = useModelStore(
useShallow((state) => ({
list.sort((a, b) => { setIsOpenLightcone: state.setIsOpenLightcone,
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity) }))
if (r !== 0) return r )
return b.ID - a.ID const { mapLightCone, baseType } = useDetailDataStore(
}) useShallow((state) => ({
mapLightCone: state.mapLightCone,
return list baseType: state.baseType,
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale]) }))
)
const transI18n = useTranslations("DataPage")
return ( const listLightcone = useMemo(() => {
<div> if (!mapLightCone || !locale) return []
<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"> let list = Object.values(mapLightCone)
{transI18n("lightConeSetting")}
</h3> if (lightconeSearch) {
</div> list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
<div className="mt-2 w-full"> }
<div className="flex items-start flex-col gap-2">
<div>Search</div> const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
<input const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)} list = list.filter(item => {
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full" const itemRank = calcRarity(item.Rarity).toString();
/> return (
</div> (allRankFalse || !!mapLightconeRankActive[itemRank] || !!mapLightconeRankActive[item.Rarity]) &&
<div className="flex items-start flex-col gap-2 mt-2 w-full"> (allPathFalse || !!mapLightconePathActive[item.BaseType])
<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]) => ( list.sort((a, b) => {
<div const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
key={key} if (r !== 0) return r
onClick={() => { return b.ID - a.ID
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" return list
style={{ }, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
}} return (
> <div className="space-y-4">
<Image {/* Search & Filters */}
unoptimized <div className="p-3 rounded-2xl bg-base-200 border border-base-300 space-y-2.5">
crossOrigin="anonymous" {/* Search Bar */}
src={`${process.env.CDN_URL}/${value.Icon}`} <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">
alt={key} <Search className="w-4 h-4 text-base-content/60 shrink-0" />
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md" <input
width={200} value={lightconeSearch}
height={200} onChange={(e) => setLightconeSearch(e.target.value)}
/> type="text"
</div> placeholder={`${transI18n("lightcones")}...`}
))} className="grow text-xs bg-transparent border-none outline-none focus:outline-none"
</div> />
{lightconeSearch && (
<div className="flex flex-wrap mb-1 mx-1 gap-2"> <button
{Object.keys(mapLightconeRankActive).map((key, index) => ( type="button"
<div onClick={() => setLightconeSearch("")}
key={index} className="text-base-content/40 hover:text-base-content"
onClick={() => { >
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] }) <X className="w-3.5 h-3.5" />
}} </button>
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={{ </label>
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
}} {/* Filter Chips */}
> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center"> {/* Path Filter Chips */}
{key}* <div className="flex flex-wrap items-center gap-1">
</div> {Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
</div> const isActive = !!mapLightconePathActive[key];
))} return (
</div> <button
</div> key={key}
type="button"
</div> onClick={() => {
</div> setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2"> }}
{listLightcone.map((item, index) => ( className={`h-7 w-7 rounded-lg transition-all duration-150 active-press border flex items-center justify-center ${
<div key={index} onClick={() => { isActive
if (avatarSelected) { ? "bg-primary/20 border-primary ring-1.5 ring-primary/50 shadow-xs scale-105"
const avatar = avatars[avatarSelected?.ID?.toString()] : "bg-base-200 hover:bg-base-300 border-base-content/10 opacity-80 hover:opacity-100"
avatar.profileList[avatar.profileSelect].lightcone = { }`}
level: 80, title={key}
item_id: item.ID, >
rank: 1, <Image
promotion: 6 unoptimized
} crossOrigin="anonymous"
setAvatar({ ...avatar }) src={`${process.env.CDN_URL}/${value.Icon}`}
setIsOpenLightcone(false) alt={key}
} className={`w-4.5 h-4.5 object-contain ${
}}> isActive ? "theme-path-icon-active" : "theme-path-icon"
<LightconeCard data={item} /> }`}
</div> width={20}
))} height={20}
</div> />
</div> </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 { ASEvent, MonsterStore } from "@/types";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function AsBar() { export default function AsBar() {
const { locale } = useLocaleStore() const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const { const {
as_config, as_config,
setAsConfig setAsConfig
} = useUserDataStore() } = useUserDataStore(
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore() 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 transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => { const challengeSelected = useMemo(() => {
@@ -108,6 +124,13 @@ export default function AsBar() {
if (targetEventList.length > 0) { if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID 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) { for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = [] const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) { for (const value of Object.values(wave)) {
@@ -135,10 +158,10 @@ export default function AsBar() {
if (!mapAS) return null if (!mapAS) return null
return ( return (
<div className="py-8 relative"> <div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */} {/* 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"> <div className="mb-4 w-full">
<SelectCustomText <SelectCustomText
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({ customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
@@ -209,13 +232,13 @@ export default function AsBar() {
</div> </div>
)} )}
{/* Turbulence Buff */} {/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20"> <div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2> <h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{challengeSelected ? ( {challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => ( challengeSelected.MazeBuff.map((buff, i) => (
<div <div
key={i} key={i}
className="text-base" className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "", 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>
</div> </div>
@@ -246,7 +269,7 @@ export default function AsBar() {
const targetEvent = eventList[0]; const targetEvent = eventList[0];
return ( 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> <h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, waveIndex) => ( {targetEvent?.MonsterList?.map((wave, waveIndex) => (
+75 -69
View File
@@ -17,19 +17,37 @@ import { useTranslations } from "next-intl";
import useGlobalStore from "@/stores/globalStore"; import useGlobalStore from "@/stores/globalStore";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { MonsterDetail } from "@/types"; import { MonsterDetail } from "@/types";
import { useShallow } from "zustand/react/shallow";
export default function CeBar() { export default function CeBar() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null); const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { ce_config, setCeConfig } = useUserDataStore() const { ce_config, setCeConfig } = useUserDataStore(
const { mapMonster, stage, damageType } = useDetailDataStore() useShallow((state) => ({
const { locale } = useLocaleStore() 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 transI18n = useTranslations("DataPage")
const [showSearchStage, setShowSearchStage] = useState(false) const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("") const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1) const [stagePage, setStagePage] = useState(1)
const { extraData, setExtraData } = useGlobalStore() const { extraData, setExtraData } = useGlobalStore(
useShallow((state) => ({
extraData: state.extraData,
setExtraData: state.setExtraData,
}))
)
const pageSize = 30 const pageSize = 30
@@ -59,10 +77,6 @@ export default function CeBar() {
[monsterPage, filteredMonsters] [monsterPage, filteredMonsters]
) )
useEffect(() => {
setMonsterPage(1)
}, [searchTerm])
const stageList = useMemo(() => Object.values(stage).map((item) => ({ const stageList = useMemo(() => Object.values(stage).map((item) => ({
id: item.ID.toString(), id: item.ID.toString(),
name: `${getLocaleName(locale, item.Name)} (${item.ID})`, name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
@@ -79,11 +93,6 @@ export default function CeBar() {
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages]) const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
useEffect(() => { useEffect(() => {
if (!ce_config) return if (!ce_config) return
if (!extraData || !extraData.theory_craft?.mode) return if (!extraData || !extraData.theory_craft?.mode) return
@@ -110,27 +119,26 @@ export default function CeBar() {
return ( 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) setShowSearchWaveId(null)
setShowSearchStage(false) setShowSearchStage(false)
}}> }}>
<div className="mb-4 w-full relative"> <div className="w-full relative">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<button <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) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setShowSearchStage(!showSearchStage) setShowSearchStage(!showSearchStage)
}} }}
> >
<Search className="w-6 h-6" /> <Search className="w-4 h-4" />
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span> <span className="text-left font-medium"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</button> </button>
</div> </div>
{showSearchStage && ( {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"> <div className="flex items-center gap-2 mb-2">
<label className="input w-full"> <label className="input w-full">
@@ -147,46 +155,44 @@ export default function CeBar() {
</g> </g>
</svg> </svg>
<input <input
type="search" className="grow"
placeholder={transI18n("searchStage")}
value={stageSearchTerm} value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)} onChange={(e) => {
autoFocus setStageSearchTerm(e.target.value)
setStagePage(1)
}}
type="search"
required
placeholder={transI18n("searchStage")}
/> />
</label> </label>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[48vh] overflow-y-auto pr-1">
<div className="max-h-60 overflow-y-auto space-y-1">
{paginatedStages.length > 0 ? ( {paginatedStages.length > 0 ? (
<> paginatedStages.map((stage) => (
{paginatedStages.map((stage) => ( <button
<div key={stage.id}
key={stage.id} className="btn btn-sm btn-outline justify-start"
className="p-2 hover:bg-primary/20 rounded cursor-pointer" onClick={(e) => {
onClick={() => { e.stopPropagation()
if (ce_config.stage_id !== Number(stage.id)) { if (ce_config.stage_id !== Number(stage.id)) {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 }) setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
} }
setShowSearchStage(false) setShowSearchStage(false)
setStageSearchTerm("") setStageSearchTerm("")
}} }}
> >
{stage.name} <span className="truncate">{stage.name}</span>
</div> </button>
))} ))
</>
) : ( ) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div> <div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)} )}
</div> </div>
{paginatedStages.length > 0 && ( {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 <button
disabled={stagePage === 1} disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2" className="btn btn-sm btn-outline btn-success"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setStagePage(stagePage - 1) setStagePage(stagePage - 1)
@@ -197,7 +203,7 @@ export default function CeBar() {
<button <button
disabled={!hasMorePages} disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2" className="btn btn-sm btn-outline btn-success"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setStagePage(stagePage + 1) setStagePage(stagePage + 1)
@@ -211,12 +217,12 @@ export default function CeBar() {
)} )}
</div> </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) => ( {ce_config.monsters.map((wave, waveIndex) => (
<div key={waveIndex} className="card border border-slate-700/50 "> <div key={waveIndex} className="rounded-xl border border-warning p-4 bg-base-100">
<div className="card-body p-6"> <div className="w-full">
<div className="flex items-center flex-wrap justify-between mb-4"> <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"> <div className="flex gap-2">
<button <button
disabled={waveIndex === 0} disabled={waveIndex === 0}
@@ -430,7 +436,10 @@ export default function CeBar() {
type="search" className="grow" type="search" className="grow"
placeholder={transI18n("searchMonster")} placeholder={transI18n("searchMonster")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => {
setSearchTerm(e.target.value)
setMonsterPage(1)
}}
autoFocus autoFocus
/> />
@@ -513,20 +522,17 @@ export default function CeBar() {
))} ))}
{/* Add New Wave Button */} {/* Add New Wave Button */}
<div className="card border border-slate-700/50 border-dashed"> <button
<div className="card-body p-8"> onClick={() => {
<button const newCeConfig = structuredClone(ce_config)
onClick={() => { newCeConfig.monsters.push([])
const newCeConfig = structuredClone(ce_config) setCeConfig(newCeConfig)
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"
}} >
className="btn btn-primary btn-lg w-full"> <Plus className="w-4 h-4" />
<Plus className="w-6 h-6 mr-2" /> <span>{transI18n("addNewWave")}</span>
{transI18n("addNewWave")} </button>
</button>
</div>
</div>
</div> </div>
</div> </div>
); );
+49 -33
View File
@@ -6,9 +6,15 @@ import AsBar from "./as";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import CeBar from "./ce"; import CeBar from "./ce";
import PeakBar from "./peak"; import PeakBar from "./peak";
import { useShallow } from "zustand/react/shallow";
export default function MonsterBar() { 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 transI18n = useTranslations("DataPage")
const navItems = [ const navItems = [
@@ -23,9 +29,9 @@ export default function MonsterBar() {
return ( return (
<div className="min-h-screen"> <div className="w-full">
{/* Header Navigation */} {/* 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"> <div className="flex items-center justify-center">
{/* Mobile Select */} {/* Mobile Select */}
<div className="block md:hidden w-full"> <div className="block md:hidden w-full">
@@ -43,38 +49,48 @@ export default function MonsterBar() {
</div> </div>
{/* Desktop Tabs */} {/* Desktop Tabs */}
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1"> <div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-2 w-full">
{navItems.map((item) => ( {navItems.map((item) => {
<button const isSelected = battle_type.toUpperCase() === item.value.toUpperCase()
key={item.name} return (
onClick={() => setBattleType(item.value.toUpperCase())} <button
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() key={item.name}
? 'bg-success/30 shadow-lg' onClick={() => setBattleType(item.value.toUpperCase())}
: 'bg-base-200/20 hover:bg-base-200/40 ' 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 <span
unoptimized className={
crossOrigin="anonymous" isSelected
src={`/icon/${item.icon}.webp`} ? ""
alt={item.name} : "theme-path-icon"
width={24} }
height={24} style={
/> isSelected
</span> ? {
<span>{item.name}</span> filter:
</button> '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>
</div> </div>
</nav> </nav>
+34 -10
View File
@@ -9,14 +9,30 @@ import Image from "next/image";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { MoCEvent, MonsterStore } from "@/types"; import { MoCEvent, MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function MocBar() { export default function MocBar() {
const { locale } = useLocaleStore() const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const { const {
moc_config, moc_config,
setMocConfig setMocConfig
} = useUserDataStore() } = useUserDataStore(
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore() 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") const transI18n = useTranslations("DataPage")
@@ -86,6 +102,14 @@ export default function MocBar() {
if (targetEventList.length > 0) { if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID 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) { for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = [] const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) { for (const value of Object.values(wave)) {
@@ -115,10 +139,10 @@ export default function MocBar() {
if (!mapMoc) return null if (!mapMoc) return null
return ( return (
<div className="py-8 relative"> <div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */} {/* 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"> <div className="mb-4 w-full">
<SelectCustomText <SelectCustomText
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({ 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> <div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
{/* Turbulence Buff */} {/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20"> <div className="pt-3 border-t border-base-content/10 space-y-2">
<div className="flex items-center space-x-2 mb-2"> <div className="flex items-center space-x-2">
<input <input
type="checkbox" type="checkbox"
checked={moc_config.use_turbulence_buff} checked={moc_config.use_turbulence_buff}
@@ -215,7 +239,7 @@ export default function MocBar() {
challengeSelected.MazeBuff.map((buff, i) => ( challengeSelected.MazeBuff.map((buff, i) => (
<div <div
key={i} key={i}
className="text-base" className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "", 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>
</div> </div>
@@ -246,7 +270,7 @@ export default function MocBar() {
const targetEvent = eventList[0]; const targetEvent = eventList[0];
return ( 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> <h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, waveIndex) => ( {targetEvent?.MonsterList?.map((wave, waveIndex) => (
+378 -286
View File
@@ -1,308 +1,400 @@
"use client" "use client";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText"; import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper"; import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";; import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image"; import Image from "next/image";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types"; import { MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PeakBar() { export default function PeakBar() {
const { locale } = useLocaleStore() const { locale } = useLocaleStore(
const { useShallow((state) => ({ locale: state.locale })),
peak_config, );
setPeakConfig const { peak_config, setPeakConfig } = useUserDataStore(
} = useUserDataStore() useShallow((state) => ({
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore() peak_config: state.peak_config,
const transI18n = useTranslations("DataPage") 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 listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()] const peak = mapPeak?.[peak_config?.event_id?.toString()];
if (!peak) return [] if (!peak) return [];
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null) return [...peak.PreLevel, peak.BossLevel].filter((it) => it != null);
}, [peak_config, mapPeak]) }, [peak_config, mapPeak]);
const eventSelected = useMemo(() => { const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()] return mapPeak?.[peak_config?.event_id?.toString()];
}, [peak_config, mapPeak]) }, [peak_config, mapPeak]);
const bossConfig = useMemo(() => { const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig; return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak]) }, [peak_config, mapPeak]);
const challengeSelected = useMemo(() => { const challengeSelected = useMemo(() => {
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id)) const challenge = structuredClone(
if ( listFloor?.find((peak) => peak?.ID === peak_config.challenge_id),
challenge );
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID if (
&& bossConfig challenge &&
&& peak_config?.boss_mode === "Hard" challenge.ID ===
) { mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID &&
return { challenge: bossConfig, isBoss: true } 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 { newBattleConfig.monsters.push(newWave);
challenge: challenge, }
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
}
}, [peak_config, listFloor, mapPeak, bossConfig])
useEffect(() => { setPeakConfig(newBattleConfig);
if (!challengeSelected.challenge) return }
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) { // eslint-disable-next-line react-hooks/exhaustive-deps
const newBattleConfig = structuredClone(peak_config) }, [
newBattleConfig.cycle_count = 6 peak_config.event_id,
newBattleConfig.blessings = [] peak_config.challenge_id,
for (const value of challengeSelected?.challenge?.MazeBuff) { peak_config.buff_id,
newBattleConfig.blessings.push({ peak_config.boss_mode,
id: value.ID, mapPeak,
level: 1 ]);
})
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({ </div>
id: peak_config.buff_id, {/* Settings */}
level: 1 <div
}) className={`grid grid-cols-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
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""} ${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
gap-4 mb-2 justify-items-center items-center w-full`} 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 })
}
> >
<option value={0} disabled={true}>
<div className="flex items-center gap-2 w-full"> {transI18n("selectSide")}
<label className="label"> </option>
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span> <option value="Normal">{transI18n("normalMode")}</option>
</label> <option value="Hard">{transI18n("hardMode")}</option>
<select </select>
value={peak_config.challenge_id} </div>
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>
)} )}
</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 { MonsterStore, PFEvent } from "@/types";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PfBar() { export default function PfBar() {
const { locale } = useLocaleStore() const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const { const {
pf_config, pf_config,
setPfConfig setPfConfig
} = useUserDataStore() } = useUserDataStore(
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore() 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 transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => { const challengeSelected = useMemo(() => {
@@ -89,6 +105,14 @@ export default function PfBar() {
if (targetEventList.length > 0) { if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID 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) { for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = [] const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) { for (const value of Object.values(wave)) {
@@ -116,10 +140,10 @@ export default function PfBar() {
if (!mapPF) return null if (!mapPF) return null
return ( return (
<div className="py-8 relative"> <div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */} {/* 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"> <div className="mb-4 w-full">
<SelectCustomText <SelectCustomText
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({ customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
@@ -190,16 +214,14 @@ export default function PfBar() {
</div> </div>
)} )}
{/* Turbulence Buff */} {/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20"> <div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2> <h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? ( {eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => ( eventSelected.SubOption.map((subOption, index) => (
<div key={index}> <div key={index} className="space-y-1">
<label className="label"> <span className="font-bold text-success text-sm">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
</label>
<div <div
className="text-base" className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, subOption.Desc) || "", getLocaleName(locale, subOption.Desc) || "",
@@ -213,7 +235,7 @@ export default function PfBar() {
challengeSelected?.MazeBuff?.map((buff, i) => ( challengeSelected?.MazeBuff?.map((buff, i) => (
<div <div
key={i} key={i}
className="text-base" className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "", 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>
</div> </div>
{/* Enemy Waves */} {/* Enemy Waves */}
{(pf_config?.challenge_id ?? 0) !== 0 && ( {(pf_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => { {floorSideList.map((side, i) => {
@@ -245,7 +266,7 @@ export default function PfBar() {
const targetEvent = eventList[0]; const targetEvent = eventList[0];
return ( 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> <h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => ( {targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
+15 -15
View File
@@ -1,16 +1,16 @@
'use client' 'use client'
import { parseRuby } from "@/helper"; import { parseRuby } from "@/helper";
interface TextProps { interface TextProps {
text: string; text: string;
locale: string; locale: string;
className?: string; className?: string;
} }
export default function ParseText({ text, locale, className }: TextProps) { export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") { if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />; return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
} }
return <div className={className}>{text}</div>; return <div className={className}>{text}</div>;
} }
+11 -11
View File
@@ -1,12 +1,12 @@
"use client"; "use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'; import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) { export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient()); const [queryClient] = React.useState(() => new QueryClient());
return ( return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
); );
} }
+505 -472
View File
@@ -1,473 +1,506 @@
"use client" "use client"
import NextImage from "next/image" import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo } from "react"; import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper"; import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant"; import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase"; import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore"; import useCurrentDataStore from "@/stores/currentDataStore";
import { useShallow } from "zustand/react/shallow";
export default function QuickView() {
const { avatars } = useUserDataStore() export default function QuickView() {
const transI18n = useTranslations("DataPage") const { avatars } = useUserDataStore(
const { locale } = useLocaleStore() useShallow((state) => ({ avatars: state.avatars }))
const { avatarSelected, } = useCurrentDataStore() )
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore() const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
const avatarSkillTree = useMemo(() => { )
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {} const { avatarSelected } = useCurrentDataStore(
if (avatars[avatarSelected?.ID?.toString()].enhanced) { useShallow((state) => ({ avatarSelected: state.avatarSelected }))
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {} )
} const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore(
return avatarSelected?.SkillTrees || {} useShallow((state) => ({
}, [avatarSelected, avatars]) mainAffix: state.mainAffix,
subAffix: state.subAffix,
const avatarData = useMemo(() => { mapRelicSet: state.mapRelicSet,
if (!avatarSelected) return mapLightCone: state.mapLightCone,
return avatars[avatarSelected?.ID?.toString()] mapAvatar: state.mapAvatar,
}, [avatarSelected, avatars]) }))
)
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return const avatarSkillTree = useMemo(() => {
return avatarData?.profileList?.[avatarData?.profileSelect] if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
}, [avatarSelected, avatarData]) if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
const relicEffects = useMemo(() => { }
const avatar = avatars[avatarSelected?.ID?.toString() || ""]; return avatarSelected?.SkillTrees || {}
const relicCount: { [key: string]: number } = {}; }, [avatarSelected, avatars])
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) { const avatarData = useMemo(() => {
if (relicCount[relic.relic_set_id]) { if (!avatarSelected) return
relicCount[relic.relic_set_id]++; return avatars[avatarSelected?.ID?.toString()]
} else { }, [avatarSelected, avatars])
relicCount[relic.relic_set_id] = 1;
} const avatarProfile = useMemo(() => {
} if (!avatarSelected || !avatarData) return
} return avatarData?.profileList?.[avatarData?.profileSelect] ?? avatarData?.profileList?.[0]
const listEffects: { key: string, count: number }[] = []; }, [avatarSelected, avatarData])
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) { const relicEffects = useMemo(() => {
listEffects.push({ key: key, count: value }); const avatar = avatars[avatarSelected?.ID?.toString() || ""];
} const relicCount: { [key: string]: number } = {};
}); const profile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
return listEffects; if (profile?.relics) {
}, [avatars, avatarSelected]); for (const relic of Object.values(profile.relics)) {
if (relic && relic.relic_set_id) {
const relicStats = useMemo(() => { relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return }
}
return Object.entries(avatarProfile?.relics).map(([key, value]) => { }
const mainAffixMap = mainAffix["5" + key] const listEffects: { key: string, count: number }[] = [];
const subAffixMap = subAffix["5"] Object.entries(relicCount).forEach(([key, value]) => {
if (!mainAffixMap || !subAffixMap) return if (value >= 2) {
return { listEffects.push({ key: key, count: value });
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`, }
mainAffix: { });
property: mainAffixMap?.[value?.main_affix_id]?.Property, return listEffects;
level: value?.level, }, [avatars, avatarSelected]);
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property] const relicStats = useMemo(() => {
}, if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
subAffix: value?.sub_affixes?.map((subValue) => {
return { return Object.entries(avatarProfile?.relics).map(([key, value]) => {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property, const mainAffixMap = mainAffix["5" + key]
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count), const subAffixMap = subAffix["5"]
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property], if (!mainAffixMap || !subAffixMap) return
step: subValue?.step, return {
count: subValue?.count 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),
}, [avatarSelected, avatarProfile, mainAffix, subAffix]) detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
const characterStats = useMemo(() => { subAffix: value?.sub_affixes?.map((subValue) => {
if (!avatarSelected || !avatarData) return return {
const charPromotion = calcPromotion(avatarData.level) property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
const statsData: Record<string, { detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
value: number, step: subValue?.step,
base: number, count: subValue?.count
name: string, }
icon: string, })
unit: string, }
round: number })
}> = { }, [avatarSelected, avatarProfile, mainAffix, subAffix])
HP: {
value: calcBaseStatRaw( const characterStats = useMemo(() => {
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase, if (!avatarSelected || !avatarData) return
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd, const charPromotion = calcPromotion(avatarData.level)
avatarData.level
), const statsData: Record<string, {
base: calcBaseStatRaw( value: number,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase, base: number,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd, name: string,
avatarData.level icon: string,
), unit: string,
name: "HP", round: number
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png", }> = {
unit: "", HP: {
round: 0 value: calcBaseStatRaw(
}, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
ATK: { mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
value: calcBaseStatRaw( avatarData.level
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase, ),
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd, base: calcBaseStatRaw(
avatarData.level mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
), mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
base: calcBaseStatRaw( avatarData.level
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase, ),
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd, name: transI18n("hp"),
avatarData.level icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
), unit: "",
name: "ATK", round: 0
icon: "spriteoutput/ui/avatar/icon/IconAttack.png", },
unit: "", ATK: {
round: 0 value: calcBaseStatRaw(
}, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
DEF: { mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
value: calcBaseStatRaw( avatarData.level
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase, ),
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd, base: calcBaseStatRaw(
avatarData.level mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
), mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
base: calcBaseStatRaw( avatarData.level
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase, ),
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd, name: transI18n("atk"),
avatarData.level icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
), unit: "",
name: "DEF", round: 0
icon: "spriteoutput/ui/avatar/icon/IconDefence.png", },
unit: "", DEF: {
round: 0 value: calcBaseStatRaw(
}, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
SPD: { mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0, avatarData.level
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0, ),
name: "SPD", base: calcBaseStatRaw(
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png", mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
unit: "", mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
round: 1 avatarData.level
}, ),
CRITRate: { name: transI18n("def"),
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0, icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0, unit: "",
name: "CRIT Rate", round: 0
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png", },
unit: "%", SPD: {
round: 1 value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
}, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
CRITDmg: { name: transI18n("speed"),
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0, icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0, unit: "",
name: "CRIT DMG", round: 1
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png", },
unit: "%", CRITRate: {
round: 1 value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
}, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
BreakEffect: { name: transI18n("critRate"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
base: 0, unit: "%",
name: "Break Effect", round: 1
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png", },
unit: "%", CRITDmg: {
round: 1 value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
}, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
EffectRES: { name: transI18n("critDmg"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
base: 0, unit: "%",
name: "Effect RES", round: 1
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png", },
unit: "%", BreakEffect: {
round: 1 value: 0,
}, base: 0,
EnergyRate: { name: transI18n("breakEffect"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
base: 0, unit: "%",
name: "Energy Rate", round: 1
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png", },
unit: "%", EffectRES: {
round: 1 value: 0,
}, base: 0,
EffectHitRate: { name: transI18n("effectRes"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
base: 0, unit: "%",
name: "Effect Hit Rate", round: 1
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png", },
unit: "%", EnergyRate: {
round: 1 value: 0,
}, base: 0,
HealBoost: { name: transI18n("energyRegenerationRate"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
base: 0, unit: "%",
name: "Healing Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png", },
unit: "%", EffectHitRate: {
round: 1 value: 0,
}, base: 0,
PhysicalAdd: { name: transI18n("effectHitRate"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
base: 0, unit: "%",
name: "Physical Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png", },
unit: "%", HealBoost: {
round: 1 value: 0,
}, base: 0,
FireAdd: { name: transI18n("outgoingHealingBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
base: 0, unit: "%",
name: "Fire Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png", },
unit: "%", PhysicalAdd: {
round: 1 value: 0,
}, base: 0,
IceAdd: { name: transI18n("physicalDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
base: 0, unit: "%",
name: "Ice Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png", },
unit: "%", FireAdd: {
round: 1 value: 0,
}, base: 0,
ThunderAdd: { name: transI18n("fireDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
base: 0, unit: "%",
name: "Thunder Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png", },
unit: "%", IceAdd: {
round: 1 value: 0,
}, base: 0,
WindAdd: { name: transI18n("iceDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
base: 0, unit: "%",
name: "Wind Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png", },
unit: "%", ThunderAdd: {
round: 1 value: 0,
}, base: 0,
QuantumAdd: { name: transI18n("thunderDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
base: 0, unit: "%",
name: "Quantum Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png", },
unit: "%", WindAdd: {
round: 1 value: 0,
}, base: 0,
ImaginaryAdd: { name: transI18n("windDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
base: 0, unit: "%",
name: "Imaginary Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png", },
unit: "%", QuantumAdd: {
round: 1 value: 0,
}, base: 0,
ElationAdd: { name: transI18n("quantumDmgBoost"),
value: 0, icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
base: 0, unit: "%",
name: "Elation Boost", round: 1
icon: "spriteoutput/ui/avatar/icon/IconJoy.png", },
unit: "%", ImaginaryAdd: {
round: 1 value: 0,
} base: 0,
} name: transI18n("imaginaryDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) { unit: "%",
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level) round: 1
statsData.HP.value += calcBaseStatRaw( },
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP, ElationAdd: {
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd, value: 0,
avatarProfile?.lightcone?.level base: 0,
) name: transI18n("elation"),
statsData.HP.base += calcBaseStatRaw( icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP, unit: "%",
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd, round: 1
avatarProfile?.lightcone?.level }
) }
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack, if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd, const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
avatarProfile?.lightcone?.level statsData.HP.value += calcBaseStatRaw(
) mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
statsData.ATK.base += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack, avatarProfile?.lightcone?.level
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd, )
avatarProfile?.lightcone?.level statsData.HP.base += calcBaseStatRaw(
) mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
statsData.DEF.value += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence, avatarProfile?.lightcone?.level
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd, )
avatarProfile?.lightcone?.level statsData.ATK.value += calcBaseStatRaw(
) mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
statsData.DEF.base += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence, avatarProfile?.lightcone?.level
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd, )
avatarProfile?.lightcone?.level statsData.ATK.base += calcBaseStatRaw(
) mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus avatarProfile?.lightcone?.level
if (bonusData && bonusData.length > 0) { )
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed") statsData.DEF.value += calcBaseStatRaw(
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed") mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
bonusSpd.forEach((bonus) => { mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
statsData.SPD.value += bonus.Value avatarProfile?.lightcone?.level
statsData.SPD.base += bonus.Value )
}) statsData.DEF.base += calcBaseStatRaw(
bonusOther.forEach((bonus) => { mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
if (statsBase && statsData[statsBase]) { avatarProfile?.lightcone?.level
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value) )
}
}) 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")
if (avatarSkillTree) { const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
Object.values(avatarSkillTree).forEach((value) => { bonusSpd.forEach((bonus) => {
if (value?.["1"] statsData.SPD.value += bonus.Value
&& value?.["1"]?.PointID statsData.SPD.base += bonus.Value
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number" })
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0 bonusOther.forEach((bonus) => {
&& value?.["1"]?.StatusAddList const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
&& value?.["1"].StatusAddList.length > 0) { if (statsBase && statsData[statsBase]) {
value?.["1"]?.StatusAddList.forEach((status) => { statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat }
if (statsBase && statsData[statsBase]) { })
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.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
if (avatarProfile?.relics && mainAffix && subAffix) { && value?.["1"].StatusAddList.length > 0) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => { value?.["1"]?.StatusAddList.forEach((status) => {
const mainAffixMap = mainAffix["5" + key] const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
const subAffixMap = subAffix["5"] if (statsBase && statsData[statsBase]) {
if (!mainAffixMap || !subAffixMap) return statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
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]) { if (avatarProfile?.relics && mainAffix && subAffix) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base) 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]) {
if (relicEffects && relicEffects.length > 0) { statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
relicEffects.forEach((relic) => { }
const dataBonus = mapRelicSet?.[relic.key]?.Skills value?.sub_affixes.forEach((subValue) => {
if (!dataBonus || Object.keys(dataBonus).length === 0) return const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
Object.entries(dataBonus || {}).forEach(([key, value]) => { if (subStats && statsData[subStats]) {
if (relic.count < Number(key)) return statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
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) }
}
}) 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
return statsData value.Bonus.forEach((bonus) => {
}, [ const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
avatarSelected, if (statsBase && statsData[statsBase]) {
avatarData, statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
mapAvatar, }
avatarProfile?.lightcone, })
avatarProfile?.relics, })
mapLightCone, })
mainAffix, }
subAffix,
relicEffects, return statsData
mapRelicSet, }, [
avatarSkillTree avatarSelected,
]) avatarData,
mapAvatar,
return ( avatarProfile?.lightcone,
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> avatarProfile?.relics,
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3"> mapLightCone,
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base"> mainAffix,
{Object.entries(characterStats || {})?.map(([key, stat], index) => { subAffix,
if (!stat || (key.includes("Add") && stat.value === 0)) return null relicEffects,
return ( mapRelicSet,
<div key={index} className="flex flex-row items-center justify-between"> avatarSkillTree
<div className="flex flex-row items-center"> ])
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`} return (
unoptimized <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-1">
crossOrigin="anonymous" {/* Stats Overview Column - Clean Minimal Grid, No Box-in-Box */}
alt="Stat Icon" <div className="lg:col-span-6 space-y-6">
width={40} <div className="space-y-3">
height={40} <div className="flex items-center justify-between pb-2 border-b border-base-content/10">
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full" <div className="flex items-center gap-2">
/> <span className="w-1 h-3.5 bg-primary rounded-full" />
<div className="font-bold">{stat.name}</div> <h3 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
</div> {transI18n("character")}
<div className="ml-3 mr-3 grow border rounded opacity-50" /> </h3>
<div className="flex cursor-default flex-col text-right font-bold">{ </div>
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0 <span className="text-xs font-mono font-medium text-base-content/60">
}{stat.unit}</div> {transI18n("level")} {avatarData?.level || 1}
</div> </span>
) </div>
})}
<hr /> <div className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
</div> {Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
<div className="flex flex-col items-center gap-1 w-full my-2"> return (
{relicEffects.map((setEffect, index) => { <div
const relicInfo = mapRelicSet[setEffect.key]; key={index}
if (!relicInfo) return null; className="flex items-center justify-between py-1.5 border-b border-base-content/5"
return ( >
<div key={index} className="flex w-full flex-row justify-between text-left"> <div className="flex items-center gap-2 min-w-0 pr-2">
<div <NextImage
className="font-bold truncate max-w-full mr-1" src={`${process.env.CDN_URL}/${stat?.icon}`}
style={{ unoptimized
fontSize: 'clamp(0.5rem, 2vw, 1rem)' crossOrigin="anonymous"
}} alt={stat.name}
dangerouslySetInnerHTML={{ width={20}
__html: replaceByParam( height={20}
getLocaleName(locale, relicInfo.Name), 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">
<div> {stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0}
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span> <span className="text-[10px] font-normal opacity-80 ml-0.5">{stat.unit}</span>
</div> </span>
</div> </div>
) )
})} })}
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg"> {/* Set Effects - Flat Minimalist Row */}
{relicEffects.length > 0 && (
{relicStats?.map((relic, index) => { <div className="space-y-2 pt-2">
if (!relic || !avatarSelected) return null <div className="flex items-center gap-2 pb-1 border-b border-base-content/10">
return ( <span className="w-1 h-3.5 bg-amber-500 rounded-full" />
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} /> <h4 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
) {transI18n("relics")}
})} </h4>
</div>
{(!relicStats || !relicStats?.length) && ( <div className="space-y-1">
<div className="flex flex-col items-center justify-center"> {relicEffects.map((setEffect, index) => {
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10"> const relicInfo = mapRelicSet[setEffect.key];
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span> if (!relicInfo) return null;
</div> return (
</div> <div
)} key={index}
</div> className="flex items-center justify-between text-xs py-1.5 border-b border-base-content/5"
</div> >
) <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 */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar"; import RelicMaker from "../relicBar";
import { motion } from "framer-motion"; import useUserDataStore from "@/stores/userDataStore";
import useUserDataStore from "@/stores/userDataStore"; import { useTranslations } from "next-intl";
import { useTranslations } from "next-intl"; import RelicCard from "../card/relicCard";
import RelicCard from "../card/relicCard"; import useModelStore from '@/stores/modelStore';
import useModelStore from '@/stores/modelStore'; import { replaceByParam } from '@/helper';
import { replaceByParam } from '@/helper'; import useRelicMakerStore from '@/stores/relicMakerStore';
import useRelicMakerStore from '@/stores/relicMakerStore'; import QuickView from "../quickView";
import QuickView from "../quickView"; import { ModalConfig } from "@/types";
import { ModalConfig } from "@/types"; import useCurrentDataStore from "@/stores/currentDataStore";
import useCurrentDataStore from "@/stores/currentDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import useDetailDataStore from "@/stores/detailDataStore"; import { getLocaleName } from '@/helper';
import { getLocaleName } from '@/helper'; import useLocaleStore from "@/stores/localeStore";
import useLocaleStore from "@/stores/localeStore"; import { useShallow } from 'zustand/react/shallow';
export default function RelicsInfo() { export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore() const { avatars, setAvatar } = useUserDataStore(
const { useShallow((state) => ({
setSelectedRelicSlot, avatars: state.avatars,
selectedRelicSlot, setAvatar: state.setAvatar,
setSelectedMainStat, }))
setSelectedRelicSet, )
setSelectedRelicLevel, const {
setListSelectedSubStats, setSelectedRelicSlot,
resetHistory, selectedRelicSlot,
resetSubStat, setSelectedMainStat,
listSelectedSubStats, setSelectedRelicSet,
} = useRelicMakerStore() setSelectedRelicLevel,
setListSelectedSubStats,
const { resetHistory,
isOpenRelic, resetSubStat,
setIsOpenRelic, listSelectedSubStats,
isOpenQuickView, } = useRelicMakerStore(
setIsOpenQuickView useShallow((state) => ({
} = useModelStore() setSelectedRelicSlot: state.setSelectedRelicSlot,
const transI18n = useTranslations("DataPage") selectedRelicSlot: state.selectedRelicSlot,
const { avatarSelected } = useCurrentDataStore() setSelectedMainStat: state.setSelectedMainStat,
const { mapRelicSet, subAffix } = useDetailDataStore() setSelectedRelicSet: state.setSelectedRelicSet,
const { locale } = useLocaleStore() setSelectedRelicLevel: state.setSelectedRelicLevel,
setListSelectedSubStats: state.setListSelectedSubStats,
const handleShow = (modalId: string) => { resetHistory: state.resetHistory,
const modal = document.getElementById(modalId) as HTMLDialogElement | null; resetSubStat: state.resetSubStat,
if (modal) { listSelectedSubStats: state.listSelectedSubStats,
modal.showModal(); }))
} )
};
const {
// Close modal handler isOpenRelic,
const handleCloseModal = (modalId: string) => { setIsOpenRelic,
const modal = document.getElementById(modalId) as HTMLDialogElement | null; isOpenQuickView,
if (modal) { setIsOpenQuickView
modal.close(); } = useModelStore(
} useShallow((state) => ({
}; isOpenRelic: state.isOpenRelic,
setIsOpenRelic: state.setIsOpenRelic,
isOpenQuickView: state.isOpenQuickView,
const getRelic = useCallback((slot: string) => { setIsOpenQuickView: state.setIsOpenQuickView,
const avatar = avatars[avatarSelected?.ID.toString() || ""]; }))
if (avatar) { )
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null; const transI18n = useTranslations("DataPage")
} const { avatarSelected } = useCurrentDataStore(
return null; useShallow((state) => ({
}, [avatars, avatarSelected]); avatarSelected: state.avatarSelected,
}))
const handlerDeleteRelic = (slot: string) => { )
const avatar = avatars[avatarSelected?.ID.toString() || ""]; const { mapRelicSet, subAffix } = useDetailDataStore(
if (avatar) { useShallow((state) => ({
delete avatar.profileList[avatar.profileSelect].relics[slot] mapRelicSet: state.mapRelicSet,
setAvatars({ ...avatars }); subAffix: state.subAffix,
} }))
} )
const { locale } = useLocaleStore(
const handlerChangeRelic = (slot: string) => { useShallow((state) => ({
const relic = getRelic(slot) locale: state.locale,
setSelectedRelicSlot(slot) }))
resetSubStat() )
resetHistory(null)
if (relic) { const handleShow = (modalId: string) => {
setSelectedMainStat(relic.main_affix_id.toString()) const modal = document.getElementById(modalId) as HTMLDialogElement | null;
setSelectedRelicSet(relic.relic_set_id.toString()) if (modal) {
setSelectedRelicLevel(relic.level) modal.showModal();
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 || ""; // Close modal handler
newSubAffixes[index].rollCount = item.count || 0; const handleCloseModal = (modalId: string) => {
newSubAffixes[index].stepCount = item.step || 0; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
}) if (modal) {
setListSelectedSubStats(newSubAffixes) modal.close();
} else { }
setSelectedMainStat("") };
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats]; const getRelic = useCallback((slot: string) => {
newSubAffixes.forEach((item) => { const avatar = avatars[avatarSelected?.ID.toString() || ""];
item.affixId = "" if (avatar) {
item.property = "" const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
item.rollCount = 0 return profile?.relics?.[slot] || null;
item.stepCount = 0 }
}) return null;
}, [avatars, avatarSelected]);
setListSelectedSubStats(newSubAffixes)
} const handlerDeleteRelic = (slot: string) => {
setIsOpenRelic(true) const avatarKey = avatarSelected?.ID.toString() || "";
handleShow("action_detail_modal") const avatar = avatars[avatarKey];
} if (avatar) {
const profileIndex = avatar.profileSelect;
const relicEffects = useMemo(() => { const currentProfile = avatar.profileList?.[profileIndex];
const avatar = avatars[avatarSelected?.ID.toString() || ""]; if (currentProfile) {
const relicCount: { [key: string]: number } = {}; const newRelics = { ...currentProfile.relics };
if (avatar) { delete newRelics[slot];
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) { const newProfileList = [...avatar.profileList];
if (relicCount[relic.relic_set_id]) { newProfileList[profileIndex] = {
relicCount[relic.relic_set_id]++; ...currentProfile,
} else { relics: newRelics,
relicCount[relic.relic_set_id] = 1; };
} setAvatar({
} ...avatar,
} profileList: newProfileList,
const listEffects: { key: string, count: number }[] = []; });
Object.entries(relicCount).forEach(([key, value]) => { }
if (value >= 2) { }
listEffects.push({ key: key, count: value }); };
}
}); const handlerChangeRelic = (slot: string) => {
return listEffects; const relic = getRelic(slot)
}, [avatars, avatarSelected]); setSelectedRelicSlot(slot)
resetSubStat()
const modalConfigs: ModalConfig[] = [ resetHistory(null)
{ if (relic) {
id: "action_detail_modal", setSelectedMainStat(relic.main_affix_id.toString())
title: "", setSelectedRelicSet(relic.relic_set_id.toString())
isOpen: isOpenRelic, setSelectedRelicLevel(relic.level)
onClose: () => { const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
setIsOpenRelic(false) relic.sub_affixes.forEach((item, index) => {
handleCloseModal("action_detail_modal") if (newSubAffixes[index]) {
}, newSubAffixes[index].affixId = item.sub_affix_id.toString();
content: <RelicMaker /> newSubAffixes[index].property = subAffix["5"]?.[item.sub_affix_id.toString()]?.Property || "";
}, newSubAffixes[index].rollCount = item.count || 0;
{ newSubAffixes[index].stepCount = item.step || 0;
id: "quick_view_modal", }
title: transI18n("quickView").toUpperCase(), })
isOpen: isOpenQuickView, setListSelectedSubStats(newSubAffixes)
onClose: () => { } else {
setIsOpenQuickView(false) setSelectedMainStat("")
handleCloseModal("quick_view_modal") setSelectedRelicSet("")
}, setSelectedRelicLevel(15)
content: <QuickView /> const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
} newSubAffixes.forEach((item) => {
] item.affixId = ""
item.property = ""
// Handle ESC key to close modal item.rollCount = 0
useEffect(() => { item.stepCount = 0
for (const item of modalConfigs) { })
if (!item?.isOpen) {
handleCloseModal(item?.id || "") setListSelectedSubStats(newSubAffixes)
} }
} setIsOpenRelic(true)
const handleEscKey = (event: KeyboardEvent) => { handleShow("action_detail_modal")
if (event.key === 'Escape') { }
for (const item of modalConfigs) {
handleCloseModal(item?.id || "") 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) {
window.addEventListener('keydown', handleEscKey); for (const relic of Object.values(currentProfile.relics)) {
return () => window.removeEventListener('keydown', handleEscKey); if (relic) {
}, [isOpenRelic]); relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
}
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"> const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
{/* Left Section - Items Grid */} if (value >= 2) {
<div className="lg:col-span-2"> listEffects.push({ key: key, count: value });
<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> return listEffects;
{transI18n("relics")} }, [avatars, avatarSelected]);
</h2>
const modalConfigs: ModalConfig[] = [
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> {
{["1", "2", "3", "4", "5", "6"].map((item, index) => ( id: "action_detail_modal",
<div key={index} className="relative group"> title: transI18n("changeRelic") || "Cài đặt di vật",
<div isOpen: isOpenRelic,
onClick={() => { onClose: () => {
if (item === selectedRelicSlot) { setIsOpenRelic(false)
setSelectedRelicSlot("") handleCloseModal("action_detail_modal")
} else { },
setSelectedRelicSlot(item) content: <RelicMaker />
} },
handlerChangeRelic(item) {
}} id: "quick_view_modal",
className="cursor-pointer" title: transI18n("quickView").toUpperCase(),
> isOpen: isOpenQuickView,
<RelicCard onClose: () => {
slot={item} setIsOpenQuickView(false)
avatarId={avatarSelected?.ID.toString() || ""} handleCloseModal("quick_view_modal")
/> },
</div> content: <QuickView />
}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1"> ]
<button
onClick={(e) => { // Handle ESC key to close modal
e.stopPropagation() useEffect(() => {
handlerChangeRelic(item) for (const item of modalConfigs) {
}} if (!item?.isOpen) {
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200" handleCloseModal(item?.id || "")
title={transI18n("changeRelic")} }
> }
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> const handleEscKey = (event: KeyboardEvent) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /> if (event.key === 'Escape') {
</svg> for (const item of modalConfigs) {
</button> handleCloseModal(item?.id || "")
}
{getRelic(item) && ( }
<button };
onClick={(e) => {
e.stopPropagation() window.addEventListener('keydown', handleEscKey);
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) { return () => window.removeEventListener('keydown', handleEscKey);
handlerDeleteRelic(item) }, [isOpenRelic]);
}
}} return (
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200" <div className="w-full space-y-4">
title={transI18n("deleteRelic")} <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
> {/* Left Section - Relic Equipment Grid */}
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div className="lg:col-span-2 space-y-4">
<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" /> <div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-6">
</svg> <div className="flex items-center justify-between pb-3 border-b border-base-content/10">
</button> <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>
</div> <span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
</div> {transI18n("relics")}
))} </span>
</div> </h2>
<div className="grid grid-cols-2 gap-2 mt-10">
<button <button
disabled={!selectedRelicSlot} onClick={() => {
onClick={() => { setIsOpenQuickView(true)
handlerChangeRelic(selectedRelicSlot) handleShow("quick_view_modal")
}} }}
className="btn btn-info" className="btn btn-sm btn-outline btn-primary rounded-xl font-bold shadow-xs"
> >
{transI18n("changeRelic")} {transI18n("quickView")}
</button> </button>
<button </div>
disabled={!selectedRelicSlot}
onClick={(e) => { {/* 6-Slot Grid completely free of box-in-box */}
e.stopPropagation() <div className="grid grid-cols-2 sm:grid-cols-3 gap-6 py-4">
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) { {["1", "2", "3", "4", "5", "6"].map((item, index) => (
handlerDeleteRelic(selectedRelicSlot) <div
} key={index}
}} onClick={() => {
className="btn btn-error" if (item === selectedRelicSlot) {
> setSelectedRelicSlot("")
{transI18n("deleteRelic")} } else {
</button> setSelectedRelicSlot(item)
</div> }
<button handlerChangeRelic(item)
onClick={() => { }}
setIsOpenQuickView(true) className={`relative group flex flex-col items-center justify-center p-2 rounded-2xl transition-all duration-200 cursor-pointer ${
handleShow("quick_view_modal") item === selectedRelicSlot
}} ? "bg-primary/5 rounded-2xl ring-1.5 ring-primary/40"
className="btn btn-info w-full mt-2" : "hover:bg-base-content/5"
> }`}
{transI18n("quickView")} >
</button> <RelicCard
</div> slot={item}
</div> avatarId={avatarSelected?.ID.toString() || ""}
/>
{/* Right Section - Stats and Set Effects */}
<div className="space-y-6"> {/* Micro Actions */}
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
{/* Set Effects Panel */} <button
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> onClick={(e) => {
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content"> e.stopPropagation()
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> handlerChangeRelic(item)
{transI18n("setEffects")} }}
</h3> className="btn btn-xs btn-circle btn-primary shadow-xs"
title={transI18n("changeRelic")}
<div className="space-y-6"> >
{relicEffects.map((setEffect, index) => { <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
const relicInfo = mapRelicSet[setEffect.key]; <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" />
if (!relicInfo) return null; </svg>
return ( </button>
<div key={index} className="space-y-3">
<div className="flex items-center gap-2"> {getRelic(item) && (
<div <button
className="font-bold text-warning" onClick={(e) => {
dangerouslySetInnerHTML={{ e.stopPropagation()
__html: replaceByParam( if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
getLocaleName(locale, relicInfo.Name), handlerDeleteRelic(item)
[] }
) }}
}} className="btn btn-xs btn-circle btn-error shadow-xs"
/> title={transI18n("deleteRelic")}
{setEffect.count && ( >
<span className={`text-sm text-info`}> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
({setEffect.count}) <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" />
</span> </svg>
)} </button>
</div> )}
</div>
<div className="space-y-2 pl-4"> </div>
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => { ))}
if (Number(requireNum) > Number(setEffect.count)) return null; </div>
return (
<div key={requireNum} className="space-y-1"> {/* Bottom Actions Bar */}
<div className={`font-medium text-success`}> <div className="grid grid-cols-2 gap-3 pt-2">
{requireNum}-PC: <button
</div> disabled={!selectedRelicSlot}
<div onClick={() => handlerChangeRelic(selectedRelicSlot)}
className="text-sm text-base-content/80 leading-relaxed pl-4" className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
dangerouslySetInnerHTML={{ >
__html: replaceByParam( {transI18n("changeRelic")}
getLocaleName(locale, value.Desc), </button>
value.Param || [] <button
) disabled={!selectedRelicSlot}
}} onClick={(e) => {
/> e.stopPropagation()
</div> if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
) handlerDeleteRelic(selectedRelicSlot)
})} }
</div> }}
</div> className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl"
) >
})} {transI18n("deleteRelic")}
</div> </button>
</div> </div>
</div> </div>
</div> </div>
{modalConfigs.map(({ id, title, onClose, content }) => ( {/* Right Section - Set Effects Showcase */}
<dialog key={id} id={id} className="modal"> <div className="space-y-4">
<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="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-4">
<div className="sticky top-0 z-10"> <div className="pb-3 border-b border-base-content/10">
<motion.button <h3 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
whileHover={{ scale: 1.1, rotate: 90 }} <div className="w-1.5 h-5 bg-linear-to-b from-amber-400 to-amber-600 rounded-full"></div>
transition={{ duration: 0.2 }} <span className="bg-clip-text text-transparent bg-linear-to-r from-amber-400 to-amber-600 font-bold tracking-tight">
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none" {transI18n("setEffects")}
onClick={onClose} </span>
> </h3>
✕ </div>
</motion.button>
</div> {relicEffects.length > 0 ? (
<div className="divide-y divide-base-content/10">
{title && ( {relicEffects.map((setEffect, index) => {
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> const relicInfo = mapRelicSet[setEffect.key];
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"> if (!relicInfo) return null;
{title} return (
</h3> <div key={index} className="py-3.5 first:pt-0 last:pb-0 space-y-2">
</div> <div className="flex items-center justify-between">
)} <div
className="font-bold text-sm text-amber-500"
{content} dangerouslySetInnerHTML={{
</div> __html: replaceByParam(
</dialog> getLocaleName(locale, relicInfo.Name),
))} []
)
</div> }}
); />
} {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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import type { SingleValue } from "react-select" import type { SingleValue } from "react-select"
import Image from "next/image" import Image from "next/image"
import useLocaleStore from "@/stores/localeStore" import useLocaleStore from "@/stores/localeStore"
import ParseText from "../parseText" import { useShallow } from "zustand/react/shallow"
import { themeColors } from "@/constant/constant" import ParseText from "../parseText"
import type { Props as SelectProps } from "react-select" import { themeColors } from "@/constant/constant"
import { JSX } from "react" import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default), const Select = dynamic(
{ ssr: false } () => import("react-select").then(m => m.default),
) as <Option, IsMulti extends boolean = false>( { ssr: false }
props: SelectProps<Option, IsMulti> ) as <Option, IsMulti extends boolean = false>(
) => JSX.Element props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string export type SelectOption = {
label: string value: string
imageUrl: string label: string
} imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[] type SelectCustomProp = {
excludeSet: SelectOption[] customSet: SelectOption[]
selectedCustomSet: string excludeSet: SelectOption[]
placeholder: string selectedCustomSet: string
setSelectedCustomSet: (value: string) => void placeholder: string
} setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const { locale, theme } = useLocaleStore() const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore(
const c = themeColors[theme] || themeColors.winter useShallow((state) => ({
locale: state.locale,
const customStyles = { theme: state.theme,
option: (p: any, s: any) => ({ }))
...p, )
display: 'flex',
alignItems: 'center', const c = themeColors[theme] || themeColors.winter
gap: '8px',
padding: '8px 12px', const customStyles = {
backgroundColor: s.isFocused ? c.bgHover : c.bg, option: (p: any, s: any) => ({
color: c.text, ...p,
cursor: 'pointer' display: 'flex',
}), alignItems: 'center',
gap: '8px',
singleValue: (p: any) => ({ padding: '10px 14px',
...p, backgroundColor: s.isFocused ? c.bgHover : c.bg,
display: 'flex', color: c.text,
alignItems: 'center', cursor: 'pointer',
gap: '8px', opacity: 1,
color: c.text }),
}),
singleValue: (p: any) => ({
control: (p: any) => ({ ...p,
...p, display: 'flex',
backgroundColor: c.bg, alignItems: 'center',
borderColor: c.border, gap: '8px',
boxShadow: 'none' color: c.text,
}), opacity: 1,
}),
menu: (p: any) => ({
...p, control: (p: any) => ({
backgroundColor: c.bg, ...p,
color: c.text, backgroundColor: c.bg,
zIndex: 9999 borderColor: c.border,
}), boxShadow: 'none',
opacity: 1,
menuPortal: (p: any) => ({ minHeight: '38px',
...p, borderRadius: '12px',
zIndex: 9999 }),
})
} menu: (p: any) => ({
...p,
const formatOptionLabel = (option: SelectOption) => ( backgroundColor: c.bg,
<div className="flex items-center gap-1 w-full h-full"> color: c.text,
<Image zIndex: 99999,
unoptimized opacity: 1,
crossOrigin="anonymous" border: `1px solid ${c.border}`,
src={option.imageUrl} boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)',
alt="" borderRadius: '12px',
width={125} overflow: 'hidden',
height={125} }),
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/> menuList: (p: any) => ({
<ParseText className='font-bold' text={option.label} locale={locale} /> ...p,
</div> backgroundColor: c.bg,
) padding: 0,
opacity: 1,
return ( }),
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))} menuPortal: (p: any) => ({
value={options.find(opt => { ...p,
return opt.value === selectedCustomSet zIndex: 99999,
}) || null} opacity: 1,
onChange={(selected: SingleValue<SelectOption>) => { })
setSelectedCustomSet(selected?.value || '') }
}}
formatOptionLabel={formatOptionLabel} const formatOptionLabel = (option: SelectOption) => (
styles={customStyles} <div className="flex items-center gap-1 w-full h-full">
placeholder={placeholder} <Image
className="my-react-select-container" unoptimized
classNamePrefix="my-react-select" crossOrigin="anonymous"
isSearchable src={option.imageUrl}
isClearable 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 { themeColors } from '@/constant/constant'
import type { Props as SelectProps } from "react-select" import type { Props as SelectProps } from "react-select"
import { JSX } from "react" import { JSX } from "react"
import { useShallow } from "zustand/react/shallow"
const Select = dynamic( const Select = dynamic(
() => import("react-select").then(m => m.default), () => import("react-select").then(m => m.default),
@@ -32,7 +33,9 @@ type SelectCustomProp = {
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) { export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet const options: SelectOption[] = customSet
const { theme } = useLocaleStore() const { theme } = useLocaleStore(
useShallow((state) => ({ theme: state.theme }))
)
const c = themeColors[theme] || themeColors.winter const c = themeColors[theme] || themeColors.winter
const customStyles = { 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 {option.description && <div
className="text-base" className="text-base"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
File diff suppressed because it is too large Load Diff
+93 -107
View File
@@ -1,108 +1,94 @@
"use client" "use client"
import NextImage from "next/image"
import NextImage from "next/image" import { AvatarDetail, RelicShowcaseType } from "@/types";
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
export default function RelicShowcase({ relic,
relic, avatarInfo,
avatarInfo, }: {
}: { relic: RelicShowcaseType;
relic: RelicShowcaseType; avatarInfo: AvatarDetail;
avatarInfo: AvatarDetail; }) {
}) { return (
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 <div className="relative shrink-0 flex flex-col items-center">
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" <NextImage
> src={relic?.img || ""}
{/* Subtle glow overlay */} unoptimized
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div> crossOrigin="anonymous"
width={48}
<div className="flex relative"> height={48}
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div> alt="Relic"
<NextImage className="h-12 w-12 object-contain"
src={relic?.img || ""} />
unoptimized </div>
crossOrigin="anonymous"
width={78} {/* Main Affix - Clean Display */}
height={78} <div className="flex w-20 shrink-0 flex-col items-start justify-center">
alt="Relic Icon" <div className="flex items-center gap-1">
className="h-19.5 w-19.5 rounded-lg" {relic?.mainAffix?.detail?.icon ? (
/> <NextImage
src={`${process.env.CDN_URL}/${relic.mainAffix.detail.icon}`}
<div unoptimized
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]" crossOrigin="anonymous"
style={{ width={16}
left: '0.65rem', height={16}
bottom: '-0.45rem', alt="Main Affix"
fontSize: '1.05rem', className="h-4 w-4 theme-stat-icon shrink-0 object-contain"
letterSpacing: '-0.1em', />
}} ) : null}
> <span className="text-xs font-bold text-amber-600 dark:text-amber-400 font-mono">
✦✦✦✦✦ {relic?.mainAffix?.valueAffix + (relic?.mainAffix?.detail?.unit || "")}
</div> </span>
</div> </div>
<span className="text-[10px] font-mono text-base-content/50 ml-0.5">
<div className=" flex w-1/6 flex-col items-center justify-center"> +{relic?.mainAffix?.level}
<div className="relative"> </span>
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div> </div>
<NextImage
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""} {/* Sub Affixes Grid - Clean Flat Typography */}
unoptimized <div className="grid flex-1 grid-cols-2 gap-x-3 gap-y-1">
crossOrigin="anonymous" {relic?.subAffix?.map((subAffix, index) => {
width={35} if (!subAffix) return null
height={35} const isOptimalStat = avatarInfo?.Relics?.SubAffixPropertyList?.includes(subAffix?.property)
alt="Main Affix Icon"
className="h-8.75 w-8.75" return (
/> <div
</div> key={index}
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]"> className={`flex items-center justify-between py-0.5 min-w-0 ${
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit} isOptimalStat
</span> ? "text-amber-600 dark:text-amber-400 font-semibold"
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10"> : "text-base-content/75"
+{relic?.mainAffix?.level} }`}
</span> >
</div> <div className="flex items-center gap-1 min-w-0 flex-1">
{subAffix?.detail?.icon ? (
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div> <NextImage
src={`${process.env.CDN_URL}/${subAffix.detail.icon}`}
<div className="grid w-[65%] m-2 grid-cols-2 gap-1"> unoptimized
{relic?.subAffix?.map((subAffix, index) => { crossOrigin="anonymous"
if (!subAffix) return null width={14}
return ( height={14}
<div key={index} className="flex flex-col"> alt="Sub Affix"
<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"> className="h-3.5 w-3.5 theme-stat-icon shrink-0 object-contain"
{subAffix?.detail?.icon ? ( />
<NextImage ) : null}
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""} <span className="text-xs font-mono truncate">
unoptimized +{subAffix?.valueAffix + (subAffix?.detail?.unit || "")}
crossOrigin="anonymous" </span>
width={32} </div>
height={32}
alt="Sub Affix Icon" {isOptimalStat && subAffix?.count ? (
className="h-6 w-6 shrink-0" <span className="ml-1 text-[10px] font-bold text-amber-600/80 dark:text-amber-400/80 shrink-0 font-mono">
/> ({subAffix?.count})
) : ( </span>
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0"> ) : null}
<span className="text-xs text-white/50">?</span> </div>
</div> )
)} })}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0"> </div>
+{subAffix?.valueAffix + subAffix?.detail?.unit} </div>
</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>
</>
)
} }
+402 -384
View File
@@ -1,385 +1,403 @@
"use client" "use client"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo } from "react"; import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant"; import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import Image from "next/image"; import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper'; import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant"; import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore"; import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail'; import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription"; import { SkillDescription } from "./skillDescription";
import { useShallow } from "zustand/react/shallow";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage") export default function SkillsInfo() {
const { theme } = useLocaleStore() const transI18n = useTranslations("DataPage")
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore() const { theme, locale } = useLocaleStore(
const { avatars, setAvatar } = useUserDataStore() useShallow((state) => ({ theme: state.theme, locale: state.locale }))
const { locale } = useLocaleStore() )
const traceButtons = useMemo(() => { const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore(
if (!avatarSelected) return useShallow((state) => ({
return traceButtonsInfo[avatarSelected.BaseType] avatarSelected: state.avatarSelected,
}, [avatarSelected]) skillIDSelected: state.skillIDSelected,
setSkillIDSelected: state.setSkillIDSelected,
const avatarData = useMemo(() => { }))
if (!avatarSelected) return )
return avatars[avatarSelected?.ID?.toString()] const { avatars, setAvatar } = useUserDataStore(
}, [avatarSelected, avatars]) useShallow((state) => ({
avatars: state.avatars,
const avatarSkillTree = useMemo(() => { setAvatar: state.setAvatar,
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {} }))
if (avatars[avatarSelected?.ID?.toString()].enhanced) { )
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {} const traceButtons = useMemo(() => {
} if (!avatarSelected) return
return avatarSelected?.SkillTrees || {} return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected, avatars]) }, [avatarSelected])
const skillInfo = useMemo(() => { const avatarData = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return if (!avatarSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"] return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatarSkillTree, skillIDSelected]) }, [avatarSelected, avatars])
const getTraceBuffDisplay = (status: StatusAdd) => { const avatarSkillTree = useMemo(() => {
const dataDisplay = mappingStats[status.PropertyType] if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (!dataDisplay) return "" if (avatars[avatarSelected?.ID?.toString()].enhanced) {
if (dataDisplay.unit === "%") { return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}` }
} return avatarSelected?.SkillTrees || {}
if (dataDisplay.name === "SPD") { }, [avatarSelected, avatars])
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
} const skillInfo = useMemo(() => {
return `${status.Value.toFixed(0)}${dataDisplay.unit}` if (!avatarSelected || !skillIDSelected) return
} return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || [] const getTraceBuffDisplay = (status: StatusAdd) => {
if (!avatarSelected || !avatarData) return undefined const dataDisplay = mappingStats[status.PropertyType]
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID)) if (!dataDisplay) return ""
if (avatarData.enhanced) { if (dataDisplay.unit === "%") {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID)) return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
} }
if (dataDisplay.name === "SPD") {
if (result && result.length > 0) { return `${status.Value.toFixed(1)}${dataDisplay.unit}`
return { }
isServant: false, return `${status.Value.toFixed(0)}${dataDisplay.unit}`
data: result, }
servantData: null,
} const dataLevelUpSkill = useMemo(() => {
} const skillIds: number[] = skillInfo?.LevelUpSkillID || []
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {}) if (!avatarSelected || !avatarData) return undefined
?.filter((skill) => skillIds.includes(skill.ID)) let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
if (resultServant && resultServant.length > 0) { result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
return { }
isServant: true,
data: resultServant, if (result && result.length > 0) {
servantData: avatarSelected.Memosprite, return {
} isServant: false,
} data: result,
return undefined servantData: null,
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData]) }
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
const handlerMaxAll = () => { ?.filter((skill) => skillIds.includes(skill.ID))
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed")) if (resultServant && resultServant.length > 0) {
return return {
} isServant: true,
const newData = structuredClone(avatarData) data: resultServant,
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => { servantData: avatarSelected.Memosprite,
const firstEntry = Object.values(dataPointEntry)[0]; }
if (firstEntry) { }
acc[firstEntry.PointID] = firstEntry.MaxLevel; return undefined
} }, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess")) const handlerMaxAll = () => {
setAvatar(newData) if (!avatarData || !avatarSkillTree) {
} toast.error(transI18n("maxAllFailed"))
return
const handlerChangeStatusTrace = (status: boolean) => { }
if (!avatarData || !skillInfo) return const newData = structuredClone(avatarData)
const newData = structuredClone(avatarData) newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0 const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) { acc[firstEntry.PointID] = firstEntry.MaxLevel;
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => { }
if (avatarSkillTree?.[pointId]?.["1"]) { return acc;
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0 }, {} as Record<string, number>)
} toast.success(transI18n("maxAllSuccess"))
}) setAvatar(newData)
} }
setAvatar(newData)
} const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
return ( const newData = structuredClone(avatarData)
<div className="w-full"> newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="rounded-xl p-6 shadow-lg"> if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content"> traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> if (avatarSkillTree?.[pointId]?.["1"]) {
{transI18n("skills")} newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
</h2> }
<div className="flex flex-col items-center"> })
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button> }
{traceButtons && avatarSelected && ( setAvatar(newData)
<div className="grid col-span-4 relative w-full aspect-square"> }
<Image
unoptimized return (
crossOrigin="anonymous" <div className="w-full space-y-4">
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`} <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-6">
alt="" {/* Left Panel: Skill Tree Visualizer */}
width={312} <div className="lg:col-span-6 space-y-4">
priority={true} <div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-4">
height={312} <div className="flex items-center justify-between pb-3 border-b border-base-300">
style={{ <h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none" <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">
className={`w-full h-full object-cover rounded-xl`} {transI18n("skills")}
/> </span>
{traceButtons.map((btn, index) => { </h2>
if (!avatarSelected?.SkillTrees?.[btn.id]) { <button
return null className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs active-press"
} onClick={handlerMaxAll}
return ( >
<div {transI18n("maxAll")}
key={`${btn.id} + ${index}`} </button>
id={btn.id} </div>
className={`
absolute rounded-full border border-black {/* Visual Skill Tree Container */}
bg-no-repeat bg-contain <div className="flex justify-center items-center py-2">
cursor-pointer transition-all duration-200 ease-in-out {traceButtons && avatarSelected && (
shadow-[0_0_5px_white] flex justify-center items-center <div className="relative w-full max-w-md aspect-square">
hover:scale-110z-10 <Image
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""} unoptimized
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""} crossOrigin="anonymous"
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""} src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""} alt="Skill Tree"
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""} width={312}
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""} priority={true}
${skillIDSelected === btn.id ? "border-4 border-primary" : ""} height={312}
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] style={{
? "opacity-50 cursor-not-allowed" filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
: ""} }}
`} className="w-full h-full object-cover"
onClick={() => { />
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id) {traceButtons.map((btn, index) => {
}} if (!avatarSelected?.SkillTrees?.[btn.id]) {
style={{ return null
left: btn.left, }
top: btn.top, const isSelected = skillIDSelected === btn.id;
transform: "translate(-50%, -50%)", const isLearned = !!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID];
}}
> const isMajor = btn.size === "big" || btn.size === "memory" || btn.size === "elation";
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`} return (
alt={btn.id.replaceAll("Point", "")} <div
priority={true} key={`${btn.id}-${index}`}
unoptimized id={btn.id}
crossOrigin="anonymous" className={`
width={124} absolute rounded-full border border-black/70 cursor-pointer transition-all duration-150 ease-out flex justify-center items-center select-none overflow-visible
height={124} ${btn.size === "small" ? "w-[6.5vw] h-[6.5vw] md:w-[2.2vw] md:h-[2.2vw] bg-white" : ""}
style={{ ${btn.size === "medium" ? "w-[8.5vw] h-[8.5vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
objectFit: "contain", ${btn.size === "big" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
padding: "2px", ${btn.size === "special" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : "" ${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"}
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && ( ${!isLearned ? "opacity-40 grayscale" : "shadow-[0_0_6px_rgba(255,255,255,0.4)]"}
<p className=" `}
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl onClick={() => {
font-bold text-center rounded-full absolute setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
translate-y-full mt-1 }}
bg-base-300 px-1 style={{
left-1/2 transform -translate-x-1/2 left: btn.left,
"> top: btn.top,
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`} transform: "translate(-50%, -50%)",
</p> }}
>
)} <Image
{btn.size === "special" && ( src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
<div alt={btn.id.replaceAll("Point", "")}
style={{ priority={true}
position: "absolute", unoptimized
inset: 0, crossOrigin="anonymous"
backgroundColor: "#4a6eff", width={124}
mixBlendMode: "screen", height={124}
opacity: 0.8, style={{
borderRadius: "50%" objectFit: "contain",
}} padding: "2px",
/> filter: !isMajor ? "brightness(0%)" : ""
)} }}
{btn.size === "memory" && ( />
<div
style={{ {/* Skill Type Color Overlays */}
position: "absolute", {btn.size === "special" && (
inset: 0, <div
backgroundColor: "#9a89ff", className="absolute inset-0 rounded-full pointer-events-none"
mixBlendMode: "screen", style={{
opacity: 0.4, backgroundColor: "#4a6eff",
borderRadius: "50%" mixBlendMode: "screen",
}} opacity: 0.8,
/> }}
)} />
{btn.size === "elation" && ( )}
<div {btn.size === "memory" && (
style={{ <div
position: "absolute", className="absolute inset-0 rounded-full pointer-events-none"
inset: 0, style={{
backgroundColor: "#ff8c00", backgroundColor: "#9a89ff",
mixBlendMode: "screen", mixBlendMode: "screen",
opacity: 0.5, opacity: 0.45,
borderRadius: "50%" }}
}} />
/> )}
)} {btn.size === "elation" && (
{btn.size === "big" && ( <div
<div className="absolute inset-0 rounded-full pointer-events-none"
style={{ style={{
position: "absolute", backgroundColor: "#ff8c00",
inset: 0, mixBlendMode: "screen",
backgroundColor: "#f5e4b0", opacity: 0.5,
mixBlendMode: "screen", }}
opacity: 0.3, />
borderRadius: "50%" )}
}} {btn.size === "big" && (
/> <div
)} className="absolute inset-0 rounded-full pointer-events-none"
</div> style={{
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
) opacity: 0.35,
})} }}
</div> />
)} )}
{!traceButtons && avatarSelected && ( {isMajor && (
<div className="flex flex-col relative w-full aspect-square"> <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}`}
</div> </p>
)} )}
</div> </div>
)
</div> })}
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> </div>
<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> </div>
{transI18n("details")} </div>
</h2> </div>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
<div> {/* Right Panel: Skill Details */}
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? ( <div className="lg:col-span-6 space-y-4">
<div> <div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-5">
<div className="font-bold text-success">{transI18n("level")}</div> <div className="pb-3 border-b border-base-300">
<div className="w-full max-w-xs"> <h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<input type="range" <div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
min={1} <span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
max={skillInfo?.MaxLevel || 1} {transI18n("details")}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1} </span>
onChange={(e) => { </h2>
const newData = structuredClone(avatarData) </div>
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData) {skillIDSelected && avatarSelected?.SkillTrees && avatarData ? (
}} <div className="space-y-4">
className="range range-success" {/* Level Slider */}
step="1" /> {skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div className="flex justify-between px-2.5 mt-2 text-xs"> <div className="space-y-1.5 pb-2">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => ( <div className="text-sm font-bold text-base-content/80">
<span key={index}>{index}</span> {transI18n("level")}
))} </div>
</div> <div className="w-full max-w-sm space-y-1">
</div> <input
</div> type="range"
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? ( min={1}
<div className="flex items-center gap-2"> max={skillInfo?.MaxLevel || 1}
<input value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
type="checkbox" onChange={(e) => {
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1} const newData = structuredClone(avatarData)
className="toggle toggle-success" newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
onChange={(e) => { setAvatar(newData)
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") { }}
if (e.target.checked) { className="range range-primary range-xs w-full"
const newData = structuredClone(avatarData) step="1"
newData.data.skills[skillInfo?.PointID] = 1 />
setAvatar(newData) <div className="flex justify-between px-1 text-[11px] font-mono text-base-content/50">
return {Array.from({ length: skillInfo?.MaxLevel || 1 }, (_, index) => index + 1).map((idx) => (
} <span key={idx}>{idx}</span>
const newData = structuredClone(avatarData) ))}
delete newData.data.skills[skillInfo?.PointID] </div>
setAvatar(newData) </div>
return </div>
} ) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
handlerChangeStatusTrace(e.target.checked) <div className="flex items-center gap-3 pb-2">
}} <span className="text-sm font-bold text-base-content/80">{transI18n("status")}</span>
/> <input
<div className="font-bold text-success"> type="checkbox"
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")} checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
</div> className="toggle toggle-primary toggle-sm"
</div> onChange={(e) => {
) : ( if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
null if (e.target.checked) {
)} const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
{((skillInfo?.PointName && skillInfo?.PointDesc) || setAvatar(newData)
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) return
&& ( }
<div className="text-xl font-bold flex items-center gap-2 mt-2"> const newData = structuredClone(avatarData)
{getLocaleName(locale, skillInfo.PointName)} delete newData.data.skills[skillInfo?.PointID]
{skillInfo.StatusAddList.length > 0 && ( setAvatar(newData)
<div> return
{skillInfo.StatusAddList.map((status, index) => ( }
<div key={index}> handlerChangeStatusTrace(e.target.checked)
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div> }}
</div> />
))} <span className={`text-xs font-bold ${avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? 'text-primary' : 'text-base-content/40'}`}>
</div> {avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
)} </span>
</div> </div>
)} ) : null}
{skillInfo?.PointDesc && ( {/* Point Name & Stat Additions */}
<div {((skillInfo?.PointName && skillInfo?.PointDesc) ||
dangerouslySetInnerHTML={{ (skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) && (
__html: replaceByParam( <div className="space-y-1.5">
getLocaleName(locale, skillInfo?.PointDesc) || "", <h3 className="text-lg font-extrabold text-base-content">
skillInfo?.Param || [] {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">
{skillInfo?.LevelUpSkillID +{getTraceBuffDisplay(status)}
&& skillInfo?.LevelUpSkillID.length > 0 </span>
&& dataLevelUpSkill ))}
&& ( </div>
<div className="mt-2 flex flex-col gap-2"> )}
</div>
{dataLevelUpSkill?.data?.map((skill, index) => ( )}
<div key={index}>
{/* Skill Description Prose */}
<div className="text-xl font-bold text-primary"> {skillInfo?.PointDesc && (
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)} <div
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`} className="text-xs sm:text-sm text-base-content/85 leading-relaxed"
</div> dangerouslySetInnerHTML={{
__html: replaceByParam(
<SkillDescription getLocaleName(locale, skillInfo?.PointDesc) || "",
skill={skill} skillInfo?.Param || []
locale={locale} )
avatarData={avatarData} }}
skillInfo={skillInfo} />
/> )}
</div>
))} {/* Level-up Skill Items (Major / Servant Skills) */}
{skillInfo?.LevelUpSkillID && skillInfo?.LevelUpSkillID.length > 0 && dataLevelUpSkill && (
</div> <div className="space-y-6 pt-1">
)} {dataLevelUpSkill?.data?.map((skill, index) => (
</div> <div key={index} className={index > 0 ? "border-t border-base-content/10 pt-4" : ""}>
)} <SkillDescription
</div> skill={skill}
</div> locale={locale}
</div> 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 { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types"; import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo"; import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: { export const SkillDescription = ({ skill, locale, avatarData, skillInfo, attackTypeLabel, skillEffectLabel }: {
skill: SkillDetail, skill: SkillDetail,
locale: string, locale: string,
avatarData: AvatarStore, avatarData: AvatarStore,
skillInfo: SkillTreePoint skillInfo: SkillTreePoint,
}) => { attackTypeLabel?: string,
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || ""; skillEffectLabel?: string
const params = skill.Level[levelKey]?.Param || []; }) => {
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc); const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const extraList = Object.values(skill.Extra).length > 0 const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
? skill.Extra
: skill?.SimpleExtra || {}; const extraList = Object.values(skill.Extra || {}).length > 0
? skill.Extra
return ( : skill?.SimpleExtra || {};
<div className="flex flex-col gap-2">
<div className="space-y-2 pb-2"> const skillName = getLocaleName(locale, skill.Name);
<div className="flex items-center gap-2">
<div className="w-1 h-5 bg-primary/80 rounded-sm" /> return (
<div <div className="space-y-2.5">
className="text-lg font-bold tracking-wide text-foreground uppercase" {/* Skill Name Heading */}
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }} <h3 className="text-xl font-bold text-base-content tracking-tight">
/> {skillName}
</div> </h3>
<div {/* Attack Type & Effect Tag */}
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent" {(attackTypeLabel || skillEffectLabel) && (
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }} <div className="text-sm font-bold text-primary flex items-center gap-1.5">
/> <span>{attackTypeLabel}</span>
</div> {skillEffectLabel && (
<span className="text-primary/70 font-medium">({skillEffectLabel})</span>
{Object.keys(extraList).length > 0 && ( )}
<ExtraEffectList extras={extraList} locale={locale} /> </div>
)} )}
</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"; "use client";
import { PropsWithChildren, useContext } from "react"; import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext"; import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) { export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>; return <div data-theme={theme} className="h-full">{children}</div>;
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./clientThemeWrapper" export * from "./clientThemeWrapper"
export * from "./themeContext" export * from "./themeContext"
+32 -40
View File
@@ -1,41 +1,33 @@
"use client"; "use client";
import { createContext, PropsWithChildren, useEffect } from "react"; import { createContext, PropsWithChildren, useCallback } from "react";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { useShallow } from "zustand/react/shallow";
interface ThemeContextType {
theme?: string; interface ThemeContextType {
changeTheme?: (nextTheme: string | null) => void; theme?: string;
} changeTheme?: (nextTheme: string | null) => void;
export const ThemeContext = createContext<ThemeContextType>({}); }
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore() const { theme, setTheme } = useLocaleStore(
useShallow((state) => ({
useEffect(() => { theme: state.theme,
if (typeof window !== "undefined") { setTheme: state.setTheme,
const storedTheme = localStorage.getItem("theme"); }))
if (storedTheme) setTheme(storedTheme); );
}
}, [setTheme]); const changeTheme = useCallback((nextTheme: string | null) => {
if (nextTheme) {
const changeTheme = (nextTheme: string | null) => { setTheme(nextTheme);
if (nextTheme) { } else {
setTheme(nextTheme); setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") { }
localStorage.setItem("theme", nextTheme); }, [theme, setTheme]);
}
} else { return (
setTheme(theme === "winter" ? "night" : "winter"); <ThemeContext.Provider value={{ theme, changeTheme }}>
if (typeof window !== "undefined") { {children}
localStorage.setItem("theme", theme); </ThemeContext.Provider>
} );
}
};
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
}; };
+220 -220
View File
@@ -1,220 +1,220 @@
export const listCurrentLanguage = { export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" }, en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" }, vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" }, ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" }, ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" }, zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" }, de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" }, es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" }, fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" }, id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" }, pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" }, ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" } th: { label: "ไทย", flag: "🇹🇭" }
}; };
export const listCurrentLanguageApi : Record<string, string> = { export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp", ja: "jp",
ko: "kr", ko: "kr",
en: "en", en: "en",
vi: "vi", vi: "vi",
zh: "cn", zh: "cn",
de: "de", de: "de",
es: "es", es: "es",
fr: "fr", fr: "fr",
id: "id", id: "id",
pt: "pt", pt: "pt",
ru: "ru", ru: "ru",
th: "th" th: "th"
}; };
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > { export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": { "HPDelta": {
name:"HP", name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png", icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "", unit: "",
baseStat: "HP" baseStat: "HP"
}, },
"AttackDelta": { "AttackDelta": {
name:"ATK", name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png", icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "", unit: "",
baseStat: "ATK" baseStat: "ATK"
}, },
"HPAddedRatio": { "HPAddedRatio": {
name:"HP", name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png", icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%", unit: "%",
baseStat: "HP" baseStat: "HP"
}, },
"AttackAddedRatio": { "AttackAddedRatio": {
name:"ATK", name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png", icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%", unit: "%",
baseStat: "ATK" baseStat: "ATK"
}, },
"DefenceDelta": { "DefenceDelta": {
name:"DEF", name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png", icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "", unit: "",
baseStat: "DEF" baseStat: "DEF"
}, },
"DefenceAddedRatio": { "DefenceAddedRatio": {
name:"DEF", name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png", icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%", unit: "%",
baseStat: "DEF" baseStat: "DEF"
}, },
"SpeedAddedRatio": { "SpeedAddedRatio": {
name:"SPD", name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png", icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%", unit: "%",
baseStat: "SPD" baseStat: "SPD"
}, },
"BaseSpeed": { "BaseSpeed": {
name:"SPD", name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png", icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "", unit: "",
baseStat: "SPD" baseStat: "SPD"
}, },
"CriticalChanceBase": { "CriticalChanceBase": {
name:"CRIT Rate", name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png", icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%", unit: "%",
baseStat: "CRITRate" baseStat: "CRITRate"
}, },
"CriticalDamageBase": { "CriticalDamageBase": {
name:"CRIT DMG", name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png", icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%", unit: "%",
baseStat: "CRITDmg" baseStat: "CRITDmg"
}, },
"HealRatioBase": { "HealRatioBase": {
name:"Outgoing Healing Boost", name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png", icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%", unit: "%",
baseStat: "HealBoost" baseStat: "HealBoost"
}, },
"StatusProbabilityBase": { "StatusProbabilityBase": {
name:"Effect Hit Rate", name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png", icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%", unit: "%",
baseStat: "EffectHitRate" baseStat: "EffectHitRate"
}, },
"StatusResistanceBase": { "StatusResistanceBase": {
name:"Effect RES", name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png", icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%", unit: "%",
baseStat: "EffectRES" baseStat: "EffectRES"
}, },
"BreakDamageAddedRatioBase": { "BreakDamageAddedRatioBase": {
name:"Break Effect", name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png", icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%", unit: "%",
baseStat: "BreakEffect" baseStat: "BreakEffect"
}, },
"SpeedDelta": { "SpeedDelta": {
name:"SPD", name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png", icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "", unit: "",
baseStat: "SPD" baseStat: "SPD"
}, },
"PhysicalAddedRatio": { "PhysicalAddedRatio": {
name:"Physical DMG Boost", name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%", unit: "%",
baseStat: "PhysicalAdd" baseStat: "PhysicalAdd"
}, },
"FireAddedRatio": { "FireAddedRatio": {
name:"Fire DMG Boost", name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%", unit: "%",
baseStat: "FireAdd" baseStat: "FireAdd"
}, },
"IceAddedRatio": { "IceAddedRatio": {
name:"Ice DMG Boost", name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%", unit: "%",
baseStat: "IceAdd" baseStat: "IceAdd"
}, },
"ThunderAddedRatio": { "ThunderAddedRatio": {
name:"Thunder DMG Boost", name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%", unit: "%",
baseStat: "ThunderAdd" baseStat: "ThunderAdd"
}, },
"WindAddedRatio": { "WindAddedRatio": {
name:"Wind DMG Boost", name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%", unit: "%",
baseStat: "WindAdd" baseStat: "WindAdd"
}, },
"QuantumAddedRatio": { "QuantumAddedRatio": {
name:"Quantum DMG Boost", name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%", unit: "%",
baseStat: "QuantumAdd" baseStat: "QuantumAdd"
}, },
"ImaginaryAddedRatio": { "ImaginaryAddedRatio": {
name:"Imaginary DMG Boost", name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png", icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%", unit: "%",
baseStat: "ImaginaryAdd" baseStat: "ImaginaryAdd"
}, },
"ElationDamageAddedRatioBase": { "ElationDamageAddedRatioBase": {
name:"Elation DMG Boost", name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png", icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%", unit: "%",
baseStat: "ElationAdd" baseStat: "ElationAdd"
}, },
"SPRatioBase": { "SPRatioBase": {
name:"Energy Regeneration Rate", name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png", icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%", unit: "%",
baseStat: "EnergyRate" baseStat: "EnergyRate"
} }
} }
export const ratioStats = [ export const ratioStats = [
"HPAddedRatio", "HPAddedRatio",
"AttackAddedRatio", "AttackAddedRatio",
"DefenceAddedRatio", "DefenceAddedRatio",
"SpeedAddedRatio", "SpeedAddedRatio",
] ]
export const mappingRelicSlot: Record<string, string> = { export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD", "1": "HEAD",
"2": "HAND", "2": "HAND",
"3": "BODY", "3": "BODY",
"4": "FOOT", "4": "FOOT",
"5": "NECK", "5": "NECK",
"6": "OBJECT", "6": "OBJECT",
} }
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = { export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: { winter: {
bg: '#ffffff', bg: '#ffffff',
bgHover: '#f1f5f9', bgHover: '#f1f5f9',
text: '#3a4f6b', text: '#3a4f6b',
border: '#cbd5e1' border: '#cbd5e1'
}, },
night: { night: {
bg: '#1d232a', bg: '#1d232a',
bgHover: '#2a323c', bgHover: '#2a323c',
text: '#cbcdd1', text: '#cbcdd1',
border: '#3f3f46' border: '#3f3f46'
}, },
cupcake: { cupcake: {
bg: '#faf7f5', bg: '#faf7f5',
bgHover: '#f3eae6', bgHover: '#f3eae6',
text: '#281333', text: '#281333',
border: '#e5d3cb' border: '#e5d3cb'
}, },
coffee: { coffee: {
bg: '#20161f', bg: '#20161f',
bgHover: '#2a1d29', bgHover: '#2a1d29',
text: '#c4a051', text: '#c4a051',
border: '#3a2a36' 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" "use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api" import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore" import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore" import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson" import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod" import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore" import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types" import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => { const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync" if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools" if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) { if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}` return `http://${serverUrl}`
} }
return serverUrl return serverUrl
} }
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => { const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({ const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" } data: { ...payload, method: "POST" }
}) as ProxyResponse | Error }) as ProxyResponse | Error
if (response instanceof Error) { if (response instanceof Error) {
return { success: false, message: response.message } return { success: false, message: response.message }
} }
if (response.error) { if (response.error) {
return { success: false, message: response.error } return { success: false, message: response.error }
} }
const parsed = psResponseSchema.safeParse(response.data) const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) { if (!parsed.success) {
return { success: false, message: "Invalid response schema" } return { success: false, message: "Invalid response schema" }
} }
return { success: true, message: "" } return { success: true, message: "" }
} }
const handleDirectServerResponse = ( const handleDirectServerResponse = (
response: PSResponse | string, response: PSResponse | string,
setIsConnectPS: (val: boolean) => void, setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void onSuccess: (extraData?: ExtraData) => void
): ActionResult => { ): ActionResult => {
if (typeof response === "string") { if (typeof response === "string") {
setIsConnectPS(false) setIsConnectPS(false)
return { success: false, message: response } return { success: false, message: response }
} }
if (response.status !== 200) { if (response.status !== 200) {
setIsConnectPS(false) setIsConnectPS(false)
return { success: false, message: response.message } return { success: false, message: response.message }
} }
setIsConnectPS(true) setIsConnectPS(true)
onSuccess(response?.extra_data) onSuccess(response?.extra_data)
return { success: true, message: "" } return { success: true, message: "" }
} }
export const connectToPS = async (): Promise<ActionResult> => { export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState() const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState() const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") { if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined }) return handleProxyRequest({ username, password, serverUrl, data: undefined })
} }
const urlQuery = getUrlQuery(connectionType, serverUrl) const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined) const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => { return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData) setExtraData(extraData)
}) })
} }
export const syncDataToPS = async (): Promise<ActionResult> => { export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState() const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.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 { 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) const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") { if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data }) return handleProxyRequest({ username, password, serverUrl, data })
} }
const urlQuery = getUrlQuery(connectionType, serverUrl) const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData) const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) { if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null if (!isEnableLua) payloadExtra.lua = null
} }
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra) const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => { return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData) const newData = structuredClone(responseExtraData)
if (newData) { if (newData) {
newData.lua = extraData?.lua || null newData.lua = extraData?.lua || null
} }
setExtraData(newData) setExtraData(newData)
}) })
} }
+14 -14
View File
@@ -1,15 +1,15 @@
export function convertToRoman(num: number): string { export function convertToRoman(num: number): string {
const roman: [number, string][] = [ const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'] [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
]; ];
let result = ''; let result = '';
for (const [val, sym] of roman) { for (const [val, sym] of roman) {
while (num >= val) { while (num >= val) {
result += sym; result += sym;
num -= val; num -= val;
} }
} }
return result; return result;
} }
+119 -119
View File
@@ -1,89 +1,89 @@
import useConnectStore from "@/stores/connectStore"; import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore"; import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types"; import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson( export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>, avatars: Record<string, AvatarStore>,
battle_type: string, battle_type: string,
moc_config: MOCConfigStore, moc_config: MOCConfigStore,
pf_config: PFConfigStore, pf_config: PFConfigStore,
as_config: ASConfigStore, as_config: ASConfigStore,
ce_config: CEConfigStore, ce_config: CEConfigStore,
peak_config: PEAKConfigStore, peak_config: PEAKConfigStore,
): FreeSRJson { ): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState() const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState() const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = [] const lightcones: LightconeJson[] = []
const relics: RelicJson[] = [] const relics: RelicJson[] = []
let battleJson: BattleConfigJson let battleJson: BattleConfigJson
if (battle_type === "MOC") { if (battle_type === "MOC") {
battleJson = { battleJson = {
battle_type: battle_type, battle_type: battle_type,
blessings: moc_config.blessings, blessings: moc_config.blessings,
custom_stats: [], custom_stats: [],
cycle_count: moc_config.cycle_count, cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id, stage_id: moc_config.stage_id,
path_resonance_id: 0, path_resonance_id: 0,
monsters: moc_config.monsters, monsters: moc_config.monsters,
} }
} else if (battle_type === "PF") { } else if (battle_type === "PF") {
battleJson = { battleJson = {
battle_type: battle_type, battle_type: battle_type,
blessings: pf_config.blessings, blessings: pf_config.blessings,
custom_stats: [], custom_stats: [],
cycle_count: pf_config.cycle_count, cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id, stage_id: pf_config.stage_id,
path_resonance_id: 0, path_resonance_id: 0,
monsters: pf_config.monsters, monsters: pf_config.monsters,
} }
} else if (battle_type === "AS") { } else if (battle_type === "AS") {
battleJson = { battleJson = {
battle_type: battle_type, battle_type: battle_type,
blessings: as_config.blessings, blessings: as_config.blessings,
custom_stats: [], custom_stats: [],
cycle_count: as_config.cycle_count, cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id, stage_id: as_config.stage_id,
path_resonance_id: 0, path_resonance_id: 0,
monsters: as_config.monsters, monsters: as_config.monsters,
} }
} else if (battle_type === "CE") { } else if (battle_type === "CE") {
battleJson = { battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT", battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings, blessings: ce_config.blessings,
custom_stats: [], custom_stats: [],
cycle_count: ce_config.cycle_count, cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id, stage_id: ce_config.stage_id,
path_resonance_id: 0, path_resonance_id: 0,
monsters: ce_config.monsters, monsters: ce_config.monsters,
} }
} else if (battle_type === "PEAK") { } else if (battle_type === "PEAK") {
battleJson = { battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT", battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings, blessings: peak_config.blessings,
custom_stats: [], custom_stats: [],
cycle_count: peak_config.cycle_count, cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id, stage_id: peak_config.stage_id,
path_resonance_id: 0, path_resonance_id: 0,
monsters: peak_config.monsters, monsters: peak_config.monsters,
} }
} else { } else {
battleJson = { battleJson = {
battle_type: battle_type, battle_type: battle_type,
blessings: [], blessings: [],
custom_stats: [], custom_stats: [],
cycle_count: 0, cycle_count: 0,
stage_id: 0, stage_id: 0,
path_resonance_id: 0, path_resonance_id: 0,
monsters: [], monsters: [],
} }
} }
const avatarsJson: { [key: string]: AvatarJson } = {} const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0 let internalUidLightcone = 0
let internalUidRelic = 0 let internalUidRelic = 0
Object.entries(avatars ?? {}).forEach(([avatarId, avatar]) => { Object.entries(avatars ?? {}).forEach(([avatarId, avatar]) => {
if (!avatar) return if (!avatar) return
@@ -91,10 +91,10 @@ export function converterToFreeSRJson(
for (const [skillId, level] of Object.entries(avatar?.data?.skills || {})) { for (const [skillId, level] of Object.entries(avatar?.data?.skills || {})) {
if (skillConfig?.[skillId]) { if (skillConfig?.[skillId]) {
skillsByAnchorType[skillConfig[skillId].IndexSlot] = level > skillConfig[skillId].MaxLevel ? skillConfig[skillId].MaxLevel : level skillsByAnchorType[skillConfig[skillId].IndexSlot] = level > skillConfig[skillId].MaxLevel ? skillConfig[skillId].MaxLevel : level
} }
} }
avatarsJson[avatarId] = { avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0), owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0), avatar_id: Number(avatar.avatar_id || 0),
data: { data: {
rank: Number(avatar.data?.rank || 0), rank: Number(avatar.data?.rank || 0),
@@ -115,21 +115,21 @@ export function converterToFreeSRJson(
const newLightcone: LightconeJson = { const newLightcone: LightconeJson = {
level: Number(currentProfile.lightcone.level || 0), level: Number(currentProfile.lightcone.level || 0),
item_id: Number(currentProfile.lightcone.item_id || 0), item_id: Number(currentProfile.lightcone.item_id || 0),
rank: Number(currentProfile.lightcone.rank || 0), rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0), promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone, internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0), equip_avatar: Number(avatar.avatar_id || 0),
} }
internalUidLightcone++ internalUidLightcone++
lightcones.push(newLightcone) lightcones.push(newLightcone)
} }
if (currentProfile.relics) { if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => { ["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot] const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) { if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = { const newRelic: RelicJson = {
level: Number(relic.level || 0), level: Number(relic.level || 0),
relic_id: Number(relic.relic_id || 0), relic_id: Number(relic.relic_id || 0),
relic_set_id: Number(relic.relic_set_id || 0), relic_set_id: Number(relic.relic_set_id || 0),
main_affix_id: Number(relic.main_affix_id || 0), main_affix_id: Number(relic.main_affix_id || 0),
@@ -137,18 +137,18 @@ export function converterToFreeSRJson(
internal_uid: internalUidRelic, internal_uid: internalUidRelic,
equip_avatar: Number(avatar.avatar_id || 0), equip_avatar: Number(avatar.avatar_id || 0),
} }
internalUidRelic++ internalUidRelic++
relics.push(newRelic) relics.push(newRelic)
} }
}) })
} }
}) })
return { return {
lightcones, lightcones,
relics, relics,
avatars: avatarsJson, avatars: avatarsJson,
battle_config: battleJson, battle_config: battleJson,
} }
} }
+61 -61
View File
@@ -1,62 +1,62 @@
import { listCurrentLanguageApi } from "@/constant/constant"; import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types"; import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations> type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string { function cleanText(text: string): string {
if (!text) return "" if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1") return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
} }
export function getNameChar( export function getNameChar(
locale: string, locale: string,
t: TFunc, t: TFunc,
data: AvatarDetail | undefined data: AvatarDetail | undefined
): string { ): string {
if (!data) return ""; if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) { if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""; return "";
} }
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase(); const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? ""; let text = data.Name[langKey] ?? "";
if (!text) { if (!text) {
text = data.Name["en"] ?? ""; text = data.Name["en"] ?? "";
} }
if (data.ID > 8000) { if (data.ID > 8000) {
text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`; text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`;
} }
return cleanText(text) return cleanText(text)
} }
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string { export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) { if (!data) {
return "" return ""
} }
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) { if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "" return ""
} }
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase(); const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? ""; let text = data[langKey] ?? "";
if (!text) { if (!text) {
text = data["en"] ?? ""; text = data["en"] ?? "";
} }
return cleanText(text) return cleanText(text)
} }
export function parseRuby(text: string): string { export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs; const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => { return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`; return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
}); });
} }
+19 -19
View File
@@ -1,23 +1,23 @@
import { AvatarDetail } from "@/types"; import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) { export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null; if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) { if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => { return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0]; const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) { if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel; acc[firstEntry.PointID] = firstEntry.MaxLevel;
} }
return acc; return acc;
}, {} as Record<string, number>) }, {} as Record<string, number>)
} }
return Object.values(avatarSelected?.SkillTrees ?? {}).reduce((acc, dataPointEntry) => { return Object.values(avatarSelected?.SkillTrees ?? {}).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0]; const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) { if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel; acc[firstEntry.PointID] = firstEntry.MaxLevel;
} }
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number>);
} }
+10 -8
View File
@@ -1,8 +1,10 @@
export * from "./getName" export * from "./getName";
export * from "./replaceByParam" export * from "./replaceByParam";
export * from "./converterToAvatarStore" export * from "./converterToAvatarStore";
export * from "./calcData" export * from "./converterToFreeSRJson";
export * from "./random" export * from "./getSkillTree";
export * from "./json" export * from "./calcData";
export * from "./convertData" export * from "./random";
export * from "./connect" 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 raw = Array.from({ length: parts }, () => Math.random());
const total = raw.reduce((a, b) => a + b, 0); const total = raw.reduce((a, b) => a + b, 0);
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1); const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0); let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) { while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) { for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) { if (diff > 0) {
result[i]++; result[i]++;
diff--; diff--;
} else if (result[i] > 1) { } else if (result[i] > 1) {
result[i]--; result[i]--;
diff++; diff++;
} }
} }
} }
return result; return result;
} }
export function randomStep(x: number): number { export function randomStep(x: number): number {
let total = 0; let total = 0;
for (let i = 0; i < x; i++) { for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3); total += Math.floor(Math.random() * 3);
} }
return total; return total;
} }
+34 -34
View File
@@ -1,39 +1,39 @@
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => { const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) { if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10); const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value; const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`; return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
} }
if (format === 'i') { if (format === 'i') {
const num = hasPercent ? value * 100 : value; const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`; return `${Math.round(num)}${hasPercent ? "%" : ""}`;
} }
return String(value); return String(value);
}; };
export function replaceByParam(desc?: string, params: number[] = []): string { export function replaceByParam(desc?: string, params: number[] = []): string {
if (!desc) return ""; if (!desc) return "";
const PARAM_REGEX = /#(\d+)\[(f(\d+)|i)\](%)?/g; const PARAM_REGEX = /#(\d+)\[(f(\d+)|i)\](%)?/g;
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => { const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1; const i = parseInt(index, 10) - 1;
const val = params[i]; const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : ""; return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
}; };
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => { let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor); const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`; return `<span style="color: ${color}">${processedInner}</span>`;
}); });
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => { result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor); return inner.replace(PARAM_REGEX, processor);
}); });
result = result.replace(PARAM_REGEX, processor); result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>"); 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 { useContext } from "react";
import { ThemeContext } from "@/components/themeController"; import { ThemeContext } from "@/components/themeController";
export const useChangeTheme = () => useContext(ThemeContext); 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 { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types"; import axios from 'axios';
import axios from 'axios'; import { psResponseSchema } from "@/zod";
import { psResponseSchema } from "@/zod"; import { ExtraData, Metadata } from "@/types";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
export async function getMetadataApi(): Promise<Metadata> { try {
try { const res = await axios.get<Metadata>(`/api/data/metadata`);
const res = await axios.get<Metadata>(`/api/data/metadata`); return res.data as Metadata;
return res.data as Metadata; } catch (error: unknown) {
} catch (error: unknown) { console.error('Failed to fetch metadata:', error);
console.error('Failed to fetch metadata:', error); return {
return { BaseType: {},
BaseType: {}, DamageType: {},
DamageType: {}, MainAffix: {},
MainAffix: {}, SubAffix: {},
SubAffix: {}, SkillConfig: {},
SkillConfig: {}, Stage: {},
Stage: {}, HardLevelConfig: {},
HardLevelConfig: {}, EliteConfig: {}
EliteConfig: {} };
}; }
} }
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> { try {
try { const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch Avatars:', error);
console.error('Failed to fetch Avatars:', error); return {};
return {}; }
} }
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> { try {
try { const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch lightcones:', error);
console.error('Failed to fetch lightcones:', error); return {};
return {}; }
} }
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> { try {
try { const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch relics:', error);
console.error('Failed to fetch relics:', error); return {};
return {}; }
} }
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> { try {
try { const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch monster:', error);
console.error('Failed to fetch monster:', error); return {};
return {}; }
} }
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> { try {
try { const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch AS:', error);
console.error('Failed to fetch AS:', error); return {};
return {}; }
} }
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> { try {
try { const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch PF:', error);
console.error('Failed to fetch PF:', error); return {};
return {}; }
} }
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> { try {
try { const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch MOC:', error);
console.error('Failed to fetch MOC:', error); return {};
return {}; }
} }
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> { try {
try { const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch peak:', error);
console.error('Failed to fetch peak:', error); return {};
return {}; }
} }
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
export async function getChangelog(): Promise<ChangelogItemType[]> { try {
try { const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`); return res.data;
return res.data; } catch (error) {
} catch (error) { console.error('Failed to fetch changelog:', error);
console.error('Failed to fetch monster:', error); return [];
return []; }
} }
}
export async function SendDataToServer(
export async function SendDataToServer( username: string,
username: string, password: string,
password: string, serverUrl: string,
serverUrl: string, data?: FreeSRJson,
data?: FreeSRJson, extraData?: ExtraData
extraData?: ExtraData ): Promise<PSResponse | string> {
): Promise<PSResponse | string> { try {
try { const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData }) const parsed = psResponseSchema.safeParse(response.data)
const parsed = psResponseSchema.safeParse(response.data) if (!parsed.success) {
if (!parsed.success) { return "Invalid response schema";
return "Invalid response schema"; }
} return parsed.data;
return parsed.data; } catch (error: unknown) {
} catch (error: any) { return (error as Error)?.message || "Unknown error";
return error?.message || "Unknown error"; }
} }
}
export async function SendDataThroughProxy({ data }: { data: Record<string, unknown> | unknown }) {
export async function SendDataThroughProxy({ data }: { data: any }) { try {
try { const response = await axios.post(`/api/proxy`, typeof data === "object" && data !== null ? { ...data } : data)
const response = await axios.post(`/api/proxy`, { ...data }) return response.data;
return response.data; } catch (error: unknown) {
} catch (error: any) { return error;
return error; }
}
} }
+37 -37
View File
@@ -1,38 +1,38 @@
import { readFileSync, readdirSync } from "fs" import { readFileSync, readdirSync } from "fs"
import path from "path" import path from "path"
type CacheItem = { type CacheItem = {
buf: Uint8Array buf: Uint8Array
type: "json" | "br" type: "json" | "br"
} }
const cache = new Map<string, CacheItem>() const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data") const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) { for (const f of readdirSync(dir)) {
const file = path.join(dir, f) const file = path.join(dir, f)
if (f.endsWith(".json.br")) { if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "") const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file)) const buf = new Uint8Array(readFileSync(file))
cache.set(name, { cache.set(name, {
buf, buf,
type: "br" type: "br"
}) })
} }
if (f.endsWith(".json")) { if (f.endsWith(".json")) {
const name = f.replace(".json", "") const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file)) const buf = new Uint8Array(readFileSync(file))
cache.set(name, { cache.set(name, {
buf, buf,
type: "json" type: "json"
}) })
} }
} }
export function getDataCache(name: string) { export function getDataCache(name: string) {
return cache.get(name) return cache.get(name)
} }
+9 -9
View File
@@ -1,10 +1,10 @@
export * from "./useFetchMetaData"; export * from "./useFetchMetaData";
export * from "./useFetchAvatarData"; export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData"; export * from "./useFetchLightconeData";
export * from "./useFetchRelicData"; export * from "./useFetchRelicData";
export * from "./useFetchMonsterData"; export * from "./useFetchMonsterData";
export * from "./useFetchPFData"; export * from "./useFetchPFData";
export * from "./useFetchMOCData"; export * from "./useFetchMOCData";
export * from "./useFetchASData"; export * from "./useFetchASData";
export * from "./useFetchPEAKData"; export * from "./useFetchPEAKData";
export * from "./useFetchChangelog"; export * from "./useFetchChangelog";
+29 -26
View File
@@ -1,26 +1,29 @@
"use client" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api' import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore() export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore(
const query = useQuery({ useShallow((state) => ({ setMapAS: state.setMapAS }))
queryKey: ['ASGroupData'], )
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data) { })
setMapAS(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load ASGroup data") if (query.data) {
} setMapAS(query.data)
}, [query.data, query.error, setMapAS]) } else if (query.error) {
toast.error("Failed to load ASGroup data")
return query }
} }, [query.data, query.error, setMapAS])
return query
}
+57 -42
View File
@@ -1,43 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */ "use client"
"use client" import { useQuery } from '@tanstack/react-query'
import { useQuery } from '@tanstack/react-query' import { getAvatarListApi } from '@/lib/api'
import { getAvatarListApi } from '@/lib/api' import { useEffect } from 'react'
import { useEffect } from 'react' import { toast } from 'react-toastify'
import { toast } from 'react-toastify' import useDetailDataStore from '@/stores/detailDataStore'
import useDetailDataStore from '@/stores/detailDataStore' import useCurrentDataStore from '@/stores/currentDataStore'
import useCurrentDataStore from '@/stores/currentDataStore' import useUserDataStore from '@/stores/userDataStore'
import useUserDataStore from '@/stores/userDataStore' import { converterToAvatarStore } from '@/helper'
import { converterToAvatarStore } from '@/helper' import { useShallow } from 'zustand/react/shallow'
export const useFetchAvatarData = () => { export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore() const { setMapAvatar, mapAvatar } = useDetailDataStore(
const { setAvatar, avatars } = useUserDataStore() useShallow((state) => ({
const { avatarSelected, setAvatarSelected } = useCurrentDataStore() setMapAvatar: state.setMapAvatar,
mapAvatar: state.mapAvatar,
const query = useQuery({ }))
queryKey: ['AvatarData'], )
queryFn: getAvatarListApi, const { setAvatars, avatars } = useUserDataStore(
staleTime: 1000 * 60 * 5, useShallow((state) => ({
}) setAvatars: state.setAvatars,
useEffect(() => { avatars: state.avatars,
const listAvatarId = Object.keys(avatars) }))
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId)) )
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist)) const { avatarSelected, setAvatarSelected } = useCurrentDataStore(
if (Object.keys(avatarStore).length === 0) return useShallow((state) => ({
for (const avatar of Object.values(avatarStore)) { avatarSelected: state.avatarSelected,
setAvatar(avatar) setAvatarSelected: state.setAvatarSelected,
} }))
}, [mapAvatar]) )
useEffect(() => { const query = useQuery({
if (query.data) { queryKey: ['AvatarData'],
setMapAvatar(query.data) queryFn: getAvatarListApi,
if (!avatarSelected) { staleTime: 1000 * 60 * 5,
setAvatarSelected(Object.values(query.data)[0]) })
} useEffect(() => {
} if (!mapAvatar || Object.keys(mapAvatar).length === 0) return
if (query.error) toast.error("Failed to load Avatar data") const listAvatarId = Object.keys(avatars)
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected]) const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
if (listAvatarNotExist.length === 0) return
return query 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" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api' import { getChangelog } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore' import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore' import useLocaleStore from '@/stores/localeStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore() export const useFetchChangelog = () => {
const { setIsChangelog } = useModelStore() const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore(
const query = useQuery({ useShallow((state) => ({
queryKey: ['changelog'], currentVersion: state.currentVersion,
queryFn: getChangelog, setChangelog: state.setChangelog,
staleTime: 1000 * 60 * 5, setCurrentVersion: state.setCurrentVersion,
}) }))
)
useEffect(() => { const { setIsChangelog } = useModelStore(
if (query.data && !query.error) { useShallow((state) => ({ setIsChangelog: state.setIsChangelog }))
setChangelog(query.data) )
if (query.data?.[0] && query.data[0].version != currentVersion) { const query = useQuery({
setIsChangelog(true) queryKey: ['changelog'],
setCurrentVersion(query.data[0].version) queryFn: getChangelog,
} staleTime: 1000 * 60 * 5,
} else if (query.error) { })
toast.error("Failed to load changelog data")
} useEffect(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps if (query.data && !query.error) {
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog]) setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
return query 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" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api'; import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'; import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore() export const useFetchLightconeData = () => {
const query = useQuery({ const { setMapLightCone } = useDetailDataStore(
queryKey: ['lightconeData'], useShallow((state) => ({ setMapLightCone: state.setMapLightCone }))
queryFn: getLightconeListApi, )
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data && !query.error) { })
setMapLightCone(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load lightcone data") if (query.data && !query.error) {
} setMapLightCone(query.data)
}, [query.data, query.error, setMapLightCone]) } else if (query.error) {
toast.error("Failed to load lightcone data")
return query }
} }, [query.data, query.error, setMapLightCone])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api' import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore() export const useFetchMOCGroupData = () => {
const query = useQuery({ const { setMapMoc } = useDetailDataStore(
queryKey: ['MOCGroupData'], useShallow((state) => ({ setMapMoc: state.setMapMoc }))
queryFn: getMOCEventListApi, )
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data && !query.error) { })
setMapMoc(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load MOCGroup data") if (query.data && !query.error) {
} setMapMoc(query.data)
}, [query.data, query.error, setMapMoc]) } else if (query.error) {
toast.error("Failed to load MOCGroup data")
return query }
} }, [query.data, query.error, setMapMoc])
return query
}
+38 -33
View File
@@ -1,33 +1,38 @@
"use client" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api' import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore' import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore() export const useFetchConfigData = () => {
const { setSettingData } = useCurrentDataStore() const { setMetaData } = useDetailDataStore(
useShallow((state) => ({ setMetaData: state.setMetaData }))
const query = useQuery({ )
queryKey: ['initialConfigData'], const { setSettingData } = useCurrentDataStore(
queryFn: async () => { useShallow((state) => ({ setSettingData: state.setSettingData }))
const metaData = await getMetadataApi() )
return { metaData }
}, const query = useQuery({
staleTime: 1000 * 60 * 5, queryKey: ['initialConfigData'],
}) queryFn: async () => {
const metaData = await getMetadataApi()
useEffect(() => { return { metaData }
if (query.data && !query.error) { },
setSettingData(query.data.metaData) staleTime: 1000 * 60 * 5,
setMetaData(query.data.metaData) })
}
else if (query.error) { useEffect(() => {
toast.error("Failed to load initial config data") if (query.data && !query.error) {
} setSettingData(query.data.metaData)
}, [query.data, query.error, setMetaData, setSettingData]) setMetaData(query.data.metaData)
}
return query 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" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api' import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore() export const useFetchMonsterData = () => {
const query = useQuery({ const { setMapMonster } = useDetailDataStore(
queryKey: ['MonsterData'], useShallow((state) => ({ setMapMonster: state.setMapMonster }))
queryFn: getMonsterListApi, )
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data && !query.error) { })
setMapMonster(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load Monster data") if (query.data && !query.error) {
} setMapMonster(query.data)
}, [query.data, query.error, setMapMonster]) } else if (query.error) {
toast.error("Failed to load Monster data")
return query }
} }, [query.data, query.error, setMapMonster])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api' import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore() export const useFetchPeakGroupData = () => {
const query = useQuery({ const { setMapPeak } = useDetailDataStore(
queryKey: ['PeakGroupData'], useShallow((state) => ({ setMapPeak: state.setMapPeak }))
queryFn: getPeakEventListApi, )
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data && !query.error) { })
setMapPeak(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load PeakGroup data") if (query.data && !query.error) {
} setMapPeak(query.data)
}, [query.data, query.error, setMapPeak]) } else if (query.error) {
toast.error("Failed to load PeakGroup data")
return query }
} }, [query.data, query.error, setMapPeak])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client" "use client"
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api' import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore' import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore() export const useFetchPFGroupData = () => {
const query = useQuery({ const { setMapPF } = useDetailDataStore(
queryKey: ['PFGroupData'], useShallow((state) => ({ setMapPF: state.setMapPF }))
queryFn: getPFEventListApi, )
staleTime: 1000 * 60 * 5, const query = useQuery({
}) queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
useEffect(() => { staleTime: 1000 * 60 * 5,
if (query.data && !query.error) { })
setMapPF(query.data)
} else if (query.error) { useEffect(() => {
toast.error("Failed to load PFGroup data") if (query.data && !query.error) {
} setMapPF(query.data)
}, [query.data, query.error, setMapPF]) } else if (query.error) {
toast.error("Failed to load PFGroup data")
return query }
} }, [query.data, query.error, setMapPF])
return query
}

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