Refactor TypeScript interfaces and types for consistency and readability
Gitea Auto Deploy / Deploy-Container (push) Successful in 49s

- Reformatted multiple TypeScript interfaces across various files to ensure consistent spacing and indentation.
- Updated `changelog.ts`, `enka.ts`, `extraData.ts`, `gloval.d.ts`, `index.ts`, `lightconeDetail.ts`, `metaData.ts`, `mics.ts`, `modelConfig.ts`, `monsterDetail.ts`, `peakDetail.ts`, `pfDetail.ts`, `psConnect.ts`, `relicDetail.ts`, `showcase.ts`, `srtools.ts`, and `zod/index.ts` for improved code clarity.
- Ensured all interfaces are properly formatted with consistent line breaks and spacing.
- Made minor adjustments to the `tsconfig.json` file for formatting consistency.
This commit is contained in:
2026-07-15 11:04:18 +07:00
parent 2b9f4c3a69
commit 7699ac7139
129 changed files with 16426 additions and 16418 deletions
+14 -14
View File
@@ -1,15 +1,15 @@
name: Gitea Auto Deploy
run-name: ${{ gitea.actor }} pushed code 🚀
on: [push]
jobs:
Deploy-Container:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Container
run: |
name: Gitea Auto Deploy
run-name: ${{ gitea.actor }} pushed code 🚀
on: [push]
jobs:
Deploy-Container:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Container
run: |
docker compose up -d --build --remove-orphans
+55 -55
View File
@@ -1,55 +1,55 @@
FROM oven/bun:canary-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Disable telemetry during the build
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Disable telemetry
ENV NEXT_TELEMETRY_DISABLED=1
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/messages ./messages
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:bun .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:bun /app/.next/standalone ./
COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# Set hostname to localhost
ENV HOSTNAME=0.0.0.0
CMD ["bun", "server.js"]
FROM oven/bun:canary-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Disable telemetry during the build
ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Disable telemetry
ENV NEXT_TELEMETRY_DISABLED=1
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/messages ./messages
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:bun .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:bun /app/.next/standalone ./
COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# Set hostname to localhost
ENV HOSTNAME=0.0.0.0
CMD ["bun", "server.js"]
+62 -62
View File
@@ -1,62 +1,62 @@
# Firefly SrTools
![Next.js](https://img.shields.io/badge/Next.js-black?style=for-the-badge&logo=next.js&logoColor=white)
![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)
**Firefly SrTools** is a modern, high-performance web toolkit designed to streamline workflows and provide essential services. This project is built upon the powerful synergy of the **Next.js** framework and the ultra-fast **Bun.js** runtime, ensuring lightning-fast development and deployment.
---
## Live Sites
The application is deployed and available at the following URLs:
| Role | URL |
| :--- | :--- |
| **Main Site** | [srtools.punklorde.org](https://srtools.punklorde.org) |
---
## Key Features
* **Optimized Performance:** Leveraging Bun for dependency management, scripting, and development server speed.
* **Next.js App Router:** Modern routing and data fetching using the latest Next.js architecture.
* **Server-Side Rendering (SSR):** Enhanced SEO and faster initial load times for improved user experience.
* **Robust Type Safety:** Built with TypeScript for reliable and maintainable code.
* **Modular Design:** Clean separation of concerns with reusable components and utilities.
## Technology Stack
* **Frontend Framework:** [Next.js 15](https://nextjs.org/) (React)
* **Runtime & Package Manager:** [Bun](https://bun.sh/)
* **Language:** TypeScript
* **Styling:** (Tailwind CSS, DaisyUI)
* **State Management:** (Zustand)
---
## Prerequisites
Ensure you have **Bun** installed on your system.
Install Bun (macOS, WSL, Linux):
```bash
curl -fsSL [https://bun.sh/install](https://bun.sh/install) | bash
```
## Development
```bash
bun install
```
```bash
bun run dev
```
```bash
bun run build
bun run start
```
# Firefly SrTools
![Next.js](https://img.shields.io/badge/Next.js-black?style=for-the-badge&logo=next.js&logoColor=white)
![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)
**Firefly SrTools** is a modern, high-performance web toolkit designed to streamline workflows and provide essential services. This project is built upon the powerful synergy of the **Next.js** framework and the ultra-fast **Bun.js** runtime, ensuring lightning-fast development and deployment.
---
## Live Sites
The application is deployed and available at the following URLs:
| Role | URL |
| :--- | :--- |
| **Main Site** | [srtools.punklorde.org](https://srtools.punklorde.org) |
---
## Key Features
* **Optimized Performance:** Leveraging Bun for dependency management, scripting, and development server speed.
* **Next.js App Router:** Modern routing and data fetching using the latest Next.js architecture.
* **Server-Side Rendering (SSR):** Enhanced SEO and faster initial load times for improved user experience.
* **Robust Type Safety:** Built with TypeScript for reliable and maintainable code.
* **Modular Design:** Clean separation of concerns with reusable components and utilities.
## Technology Stack
* **Frontend Framework:** [Next.js 15](https://nextjs.org/) (React)
* **Runtime & Package Manager:** [Bun](https://bun.sh/)
* **Language:** TypeScript
* **Styling:** (Tailwind CSS, DaisyUI)
* **State Management:** (Zustand)
---
## Prerequisites
Ensure you have **Bun** installed on your system.
Install Bun (macOS, WSL, Linux):
```bash
curl -fsSL [https://bun.sh/install](https://bun.sh/install) | bash
```
## Development
```bash
bun install
```
```bash
bun run dev
```
```bash
bun run build
bun run start
```
BIN
View File
Binary file not shown.
Binary file not shown.
+10 -2
View File
@@ -1,10 +1,18 @@
[
{
"version": "4.2.5",
"version": "4.4.0",
"date": "15/07/2026",
"type": "update",
"items": [
"New data for 4.4.0"
]
},
{
"version": "4.3.0",
"date": "01/05/2026",
"type": "update",
"items": [
"New data for 4.2.5"
"New data for 4.3.0"
]
},
{
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.
+15 -15
View File
@@ -1,15 +1,15 @@
services:
srtools-live:
build:
context: .
dockerfile: Dockerfile
container_name: srtools-live
restart: unless-stopped
ports:
- "3009:3000"
networks:
- srtools-live-network
networks:
srtools-live-network:
driver: bridge
services:
srtools-live:
build:
context: .
dockerfile: Dockerfile
container_name: srtools-live
restart: unless-stopped
ports:
- "3009:3000"
networks:
- srtools-live-network
networks:
srtools-live-network:
driver: bridge
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly-Tools von Firefly Shelter"
},
"DataPage": {
"skillType": "Fähigkeitstyp",
"skillName": "Fähigkeitsname",
"character": "Charakter",
"id": "ID",
"path": "Pfad",
"rarity": "Seltenheit",
"element": "Element",
"technique": "Technik",
"talent": "Talent",
"basic": "Standardangriff",
"skill": "Fähigkeit",
"ultimate": "Ultimate",
"servant": "Diener",
"damage": "Schaden",
"type": "Typ",
"warrior": "Die Zerstörung",
"knight": "Die Bewahrung",
"mage": "Die Gelehrsamkeit",
"priest": "Der Überfluss",
"rogue": "Die Jagd",
"shaman": "Die Harmonie",
"warlock": "Die Nichtigkeit",
"memory": "Die Erinnerung",
"elation": "Die Freude",
"fire": "Feuer",
"ice": "Eis",
"imaginary": "Imaginär",
"physical": "Physisch",
"quantum": "Quanten",
"thunder": "Blitz",
"wind": "Wind",
"hp": "LP",
"atk": "ANG",
"speed": "GES",
"critRate": "Krit. Rate",
"critDmg": "Krit. SCH",
"breakEffect": "Brucheffekt",
"effectRes": "Effekt-WDS",
"energyRegenerationRate": "Energie-Regenerationsrate",
"effectHitRate": "Effekttrefferquote",
"outgoingHealingBoost": "Ausgehender Heilungsbonus",
"fireDmgBoost": "Feuer-SCH-Bonus",
"iceDmgBoost": "Eis-SCH-Bonus",
"imaginaryDmgBoost": "Imaginär-SCH-Bonus",
"physicalDmgBoost": "Physisch-SCH-Bonus",
"quantumDmgBoost": "Quanten-SCH-Bonus",
"thunderDmgBoost": "Blitz-SCH-Bonus",
"windDmgBoost": "Wind-SCH-Bonus",
"pursued": "Zusätzlicher Schaden",
"true damage": "Wahrer Schaden",
"elationdamage": "Freude-Schaden",
"follow-up": "Folgeangriff-Schaden",
"elemental damage": "Bruch- und Superbruch-Schaden",
"dot": "Schaden über Zeit",
"qte": "QTE-Fähigkeit",
"level": "Stufe",
"relics": "Relikte",
"eidolons": "Eidolons",
"lightcones": "Lichtkegel",
"loadData": "Daten laden",
"exportData": "Daten exportieren",
"connectSetting": "Verbindungseinstellungen",
"connected": "Verbunden",
"unconnected": "Nicht verbunden",
"psConnection": "PS-Verbindung",
"connectionType": "Verbindungstyp",
"status": "Status",
"connectPs": "Mit PS verbinden",
"disconnect": "Trennen",
"other": "Andere",
"freeSr": "FreeSR",
"database": "Datenbank",
"enka": "Enka",
"monsterSetting": "Monstereinstellungen",
"serverUrl": "Server-URL",
"privateType": "Privater Typ",
"local": "Lokal",
"server": "Server",
"username": "Benutzername",
"password": "Passwort",
"placeholderServerUrl": "Server-URL eingeben",
"placeholderUsername": "Benutzernamen eingeben",
"placeholderPassword": "Passwort eingeben",
"connectedSuccess": "Erfolgreich mit PS verbunden",
"connectedFailed": "Verbindung zu PS fehlgeschlagen",
"syncSuccess": "Daten erfolgreich mit PS synchronisiert",
"syncFailed": "Fehler beim Synchronisieren",
"sync": "Synchronisieren",
"importSetting": "Import-Einstellungen",
"profile": "Profil",
"default": "Standard",
"copyProfiles": "Profile kopieren",
"addNewProfile": "Neues Profil hinzufügen",
"createNewProfile": "Neues Profil erstellen",
"editProfile": "Profil bearbeiten",
"placeholderProfileName": "Profilnamen eingeben",
"profileName": "Profilname",
"create": "Erstellen",
"update": "Aktualisieren",
"characterInformation": "Charakterinformationen",
"skills": "Fähigkeiten",
"showcaseCard": "Showcase-Karte",
"comingSoon": "Demnächst",
"characterName": "Charaktername",
"placeholderCharacter": "Charakternamen eingeben",
"characterSettings": "Charaktereinstellungen",
"levelConfiguration": "Stufenkonfiguration",
"characterLevel": "Charakterstufe",
"max": "MAX",
"ultimateEnergy": "Ultimate-Energie",
"currentEnergy": "Aktuelle Energie",
"setTo50": "Auf 50% setzen",
"battleConfiguration": "Kampfkonfiguration",
"useTechnique": "Technik verwenden",
"techniqueNote": "Technikeffekte vor dem Kampf aktivieren",
"enhancement": "Verbesserung",
"enhancementLevel": "Verbesserungsstufe",
"origin": "Ursprung",
"enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei",
"lightconeEquipment": "Lichtkegel-Ausrüstung",
"lightconeSettings": "Lichtkegel-Einstellungen",
"placeholderLevel": "Stufe eingeben",
"superimpositionRank": "Überlagerungsrang",
"ranksNote": "Höhere Ränge bieten stärkere Effekte",
"changeLightcone": "Lichtkegel wechseln",
"removeLightcone": "Lichtkegel entfernen",
"equipLightcone": "Lichtkegel ausrüsten",
"noLightconeEquipped": "Kein Lichtkegel ausgerüstet",
"equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken",
"filter": "Filter",
"selectedCharacters": "Ausgewählte Charaktere",
"selectedProfiles": "Ausgewählte Profile",
"clearAll": "Alles löschen",
"selectAll": "Alles auswählen",
"copy": "Kopieren",
"copied": "Kopiert",
"noAvatarSelected": "Kein Charakter ausgewählt",
"noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt",
"pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus",
"pleaseEnterUid": "Bitte UID eingeben",
"failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten",
"pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus",
"noDataToImport": "Keine Daten zum Importieren",
"pleaseSelectAFile": "Bitte wähle eine Datei",
"fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein",
"importEnkaDataSuccess": "Enka-Daten erfolgreich importiert",
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
"getData": "Daten abrufen",
"import": "Importieren",
"freeSRImport": "FreeSR-Import",
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
"pickAFile": "Datei auswählen",
"lightConeSetting": "Lichtkegel-Einstellung",
"relicMaker": "Relikt-Ersteller",
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
"relicSavedSuccessfully": "Relikt erfolgreich gespeichert",
"mainSettings": "Haupteinstellungen",
"mainStat": "Hauptwert",
"set": "Set",
"pleaseSelectASet": "Bitte wähle ein Set",
"effectBonus": "Effektbonus",
"totalRoll": "Gesamte Aufwertungen",
"randomizeStats": "Werte zufällig",
"randomizeRolls": "Aufwertungen zufällig",
"selectASubStat": "Wähle einen Nebenwert",
"selectASet": "Wähle ein Set",
"selectAMainStat": "Wähle einen Hauptwert",
"save": "Speichern",
"reset": "Zurücksetzen",
"roll": "Aufwertung",
"step": "Schritt",
"memoryOfChaos": "Vergessene Halle",
"pureFiction": "Reine Fiktion",
"apocalypticShadow": "Apokalyptischer Schatten",
"customEnemy": "Benutzerdefinierter Feind",
"simulatedUniverse": "Universum-Simulation",
"floor": "Ebene",
"side": "Seite",
"wave": "Welle",
"stage": "Phase",
"useCycleCount": "Zyklen-Zählung verwenden?",
"useTurbulenceBuff": "Turbulenz-Buff verwenden?",
"firstHalfEnemies": "Gegner erste Hälfte",
"secondHalfEnemies": "Gegner zweite Hälfte",
"firstNodeEnemies": "Gegner Knoten 1",
"secondNodeEnemies": "Gegner Knoten 2",
"thirdNodeEnemies": "Gegner Knoten 3",
"firstNode": "Knoten 1",
"secondNode": "Knoten 2",
"thirdNode": "Knoten 3",
"listEnemies": "Gegnerliste",
"turbulenceBuff": "Turbulenz-Buff",
"noEventSelected": "Kein Ereignis ausgewählt",
"noTurbulenceBuff": "Kein Turbulenz-Buff",
"upper": "Oben",
"lower": "Unten",
"upperToLower": "Oben -> Unten",
"lowerToUpper": "Unten -> Oben",
"selectMOCEvent": "MOC-Ereignis wählen",
"selectPFEvent": "PF-Ereignis wählen",
"selectASEvent": "AS-Ereignis wählen",
"selectCEEvent": "CE-Ereignis wählen",
"selectEvent": "Ereignis wählen",
"selectFloor": "Ebene wählen",
"selectSide": "Seite wählen",
"selectBuff": "Buff wählen",
"selectStage": "Phase wählen",
"previous": "Zurück",
"next": "Weiter",
"noMonstersFound": "Keine Monster gefunden",
"addNewWave": "Neue Welle hinzufügen",
"searchStage": "Phase suchen...",
"noStageFound": "Keine Phase gefunden",
"searchMonster": "Monster suchen...",
"changeRelic": "Relikt wechseln",
"deleteRelic": "Relikt löschen",
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
"setEffects": "Effekte einstellen",
"details": "Details",
"normal": "Standardangriff",
"bpskill": "Fähigkeit",
"maze": "Technik",
"ultra": "Ultimate",
"servantskill": "Memosprite-Fähigkeit",
"severaltalent": "Memosprite-Talent",
"singleattack": "Einzelangriff",
"enhance": "Verbessern",
"summon": "Beschwören",
"mazeattack": "Technik-Angriff",
"blast": "Explosion",
"restore": "Wiederherstellen",
"support": "Unterstützung",
"aoeattack": "Flächenangriff",
"impair": "Beeinträchtigen",
"bounce": "Abprallen",
"active": "Aktiv",
"defence": "Verteidigung",
"inactive": "Inaktiv",
"maxAll": "Alles maximieren",
"maxAllSuccess": "Fähigkeiten erfolgreich maximiert.",
"maxAllFailed": "Fehler beim Maximieren.",
"noRelicEquipped": "Kein Relikt ausgerüstet",
"anomalyArbitration": "Anomalie-Schiedsgericht",
"normalMode": "Normaler Modus",
"hardMode": "Schwerer Modus",
"selectPEAKEvent": "PEAK-Ereignis wählen",
"mode": "Modus",
"selectMode": "Modus wählen",
"rollBack": "Rückgängig",
"upRoll": "Aufwertung hoch",
"downRoll": "Aufwertung runter",
"actions": "Aktionen",
"avatars": "Avatare",
"quickView": "Schnellansicht",
"extraSetting": "Extra-Einstellungen",
"disableCensorship": "Zensur deaktivieren",
"hideUI": "UI verstecken",
"theoryCraftMode": "Theorycraft-Modus",
"cycleCount": "Zyklen-Anzahl",
"pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen",
"subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Mehrpfad-Charakter",
"mainPath": "Hauptpfad",
"march7Path": "Pfad 7. März",
"challenge": "Herausforderung",
"skipNode": "Knoten überspringen",
"disableSkip": "Überspringen deaktivieren",
"skipNode1": "Knoten 1 überspringen",
"skipNode2": "Knoten 2 überspringen",
"extraFeatures": "Zusatzfunktionen",
"detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.",
"detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.",
"detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.",
"detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.",
"detailDisableCensorship": "Deaktiviert die Zensur im Spiel.",
"detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Liste Zusatzeffekte",
"extra": "Extra",
"customLineup": "Benutzerdefinierte Aufstellung"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly-Tools von Firefly Shelter"
},
"DataPage": {
"skillType": "Fähigkeitstyp",
"skillName": "Fähigkeitsname",
"character": "Charakter",
"id": "ID",
"path": "Pfad",
"rarity": "Seltenheit",
"element": "Element",
"technique": "Technik",
"talent": "Talent",
"basic": "Standardangriff",
"skill": "Fähigkeit",
"ultimate": "Ultimate",
"servant": "Diener",
"damage": "Schaden",
"type": "Typ",
"warrior": "Die Zerstörung",
"knight": "Die Bewahrung",
"mage": "Die Gelehrsamkeit",
"priest": "Der Überfluss",
"rogue": "Die Jagd",
"shaman": "Die Harmonie",
"warlock": "Die Nichtigkeit",
"memory": "Die Erinnerung",
"elation": "Die Freude",
"fire": "Feuer",
"ice": "Eis",
"imaginary": "Imaginär",
"physical": "Physisch",
"quantum": "Quanten",
"thunder": "Blitz",
"wind": "Wind",
"hp": "LP",
"atk": "ANG",
"speed": "GES",
"critRate": "Krit. Rate",
"critDmg": "Krit. SCH",
"breakEffect": "Brucheffekt",
"effectRes": "Effekt-WDS",
"energyRegenerationRate": "Energie-Regenerationsrate",
"effectHitRate": "Effekttrefferquote",
"outgoingHealingBoost": "Ausgehender Heilungsbonus",
"fireDmgBoost": "Feuer-SCH-Bonus",
"iceDmgBoost": "Eis-SCH-Bonus",
"imaginaryDmgBoost": "Imaginär-SCH-Bonus",
"physicalDmgBoost": "Physisch-SCH-Bonus",
"quantumDmgBoost": "Quanten-SCH-Bonus",
"thunderDmgBoost": "Blitz-SCH-Bonus",
"windDmgBoost": "Wind-SCH-Bonus",
"pursued": "Zusätzlicher Schaden",
"true damage": "Wahrer Schaden",
"elationdamage": "Freude-Schaden",
"follow-up": "Folgeangriff-Schaden",
"elemental damage": "Bruch- und Superbruch-Schaden",
"dot": "Schaden über Zeit",
"qte": "QTE-Fähigkeit",
"level": "Stufe",
"relics": "Relikte",
"eidolons": "Eidolons",
"lightcones": "Lichtkegel",
"loadData": "Daten laden",
"exportData": "Daten exportieren",
"connectSetting": "Verbindungseinstellungen",
"connected": "Verbunden",
"unconnected": "Nicht verbunden",
"psConnection": "PS-Verbindung",
"connectionType": "Verbindungstyp",
"status": "Status",
"connectPs": "Mit PS verbinden",
"disconnect": "Trennen",
"other": "Andere",
"freeSr": "FreeSR",
"database": "Datenbank",
"enka": "Enka",
"monsterSetting": "Monstereinstellungen",
"serverUrl": "Server-URL",
"privateType": "Privater Typ",
"local": "Lokal",
"server": "Server",
"username": "Benutzername",
"password": "Passwort",
"placeholderServerUrl": "Server-URL eingeben",
"placeholderUsername": "Benutzernamen eingeben",
"placeholderPassword": "Passwort eingeben",
"connectedSuccess": "Erfolgreich mit PS verbunden",
"connectedFailed": "Verbindung zu PS fehlgeschlagen",
"syncSuccess": "Daten erfolgreich mit PS synchronisiert",
"syncFailed": "Fehler beim Synchronisieren",
"sync": "Synchronisieren",
"importSetting": "Import-Einstellungen",
"profile": "Profil",
"default": "Standard",
"copyProfiles": "Profile kopieren",
"addNewProfile": "Neues Profil hinzufügen",
"createNewProfile": "Neues Profil erstellen",
"editProfile": "Profil bearbeiten",
"placeholderProfileName": "Profilnamen eingeben",
"profileName": "Profilname",
"create": "Erstellen",
"update": "Aktualisieren",
"characterInformation": "Charakterinformationen",
"skills": "Fähigkeiten",
"showcaseCard": "Showcase-Karte",
"comingSoon": "Demnächst",
"characterName": "Charaktername",
"placeholderCharacter": "Charakternamen eingeben",
"characterSettings": "Charaktereinstellungen",
"levelConfiguration": "Stufenkonfiguration",
"characterLevel": "Charakterstufe",
"max": "MAX",
"ultimateEnergy": "Ultimate-Energie",
"currentEnergy": "Aktuelle Energie",
"setTo50": "Auf 50% setzen",
"battleConfiguration": "Kampfkonfiguration",
"useTechnique": "Technik verwenden",
"techniqueNote": "Technikeffekte vor dem Kampf aktivieren",
"enhancement": "Verbesserung",
"enhancementLevel": "Verbesserungsstufe",
"origin": "Ursprung",
"enhancedNote": "Höhere Verbesserungen schalten Fähigkeiten frei",
"lightconeEquipment": "Lichtkegel-Ausrüstung",
"lightconeSettings": "Lichtkegel-Einstellungen",
"placeholderLevel": "Stufe eingeben",
"superimpositionRank": "Überlagerungsrang",
"ranksNote": "Höhere Ränge bieten stärkere Effekte",
"changeLightcone": "Lichtkegel wechseln",
"removeLightcone": "Lichtkegel entfernen",
"equipLightcone": "Lichtkegel ausrüsten",
"noLightconeEquipped": "Kein Lichtkegel ausgerüstet",
"equipLightconeNote": "Rüste einen Lichtkegel aus, um deinen Charakter zu stärken",
"filter": "Filter",
"selectedCharacters": "Ausgewählte Charaktere",
"selectedProfiles": "Ausgewählte Profile",
"clearAll": "Alles löschen",
"selectAll": "Alles auswählen",
"copy": "Kopieren",
"copied": "Kopiert",
"noAvatarSelected": "Kein Charakter ausgewählt",
"noAvatarToCopySelected": "Kein Charakter zum Kopieren ausgewählt",
"pleaseSelectAtLeastOneProfile": "Bitte wähle mindestens ein Profil aus",
"pleaseEnterUid": "Bitte UID eingeben",
"failedToFetchEnkaData": "Fehler beim Abrufen der Enka-Daten",
"pleaseSelectAtLeastOneCharacter": "Bitte wähle mindestens einen Charakter aus",
"noDataToImport": "Keine Daten zum Importieren",
"pleaseSelectAFile": "Bitte wähle eine Datei",
"fileMustBeAValidJsonFile": "Die Datei muss eine gültige JSON-Datei sein",
"importEnkaDataSuccess": "Enka-Daten erfolgreich importiert",
"importFreeSRDataSuccess": "FreeSR-Daten erfolgreich importiert",
"importDatabaseSuccess": "Datenbank erfolgreich importiert",
"getData": "Daten abrufen",
"import": "Importieren",
"freeSRImport": "FreeSR-Import",
"onlySupportFreeSRJsonFile": "Unterstützt nur FreeSR-JSON-Dateien",
"pickAFile": "Datei auswählen",
"lightConeSetting": "Lichtkegel-Einstellung",
"relicMaker": "Relikt-Ersteller",
"pleaseSelectAllOptions": "Bitte wähle alle Optionen",
"relicSavedSuccessfully": "Relikt erfolgreich gespeichert",
"mainSettings": "Haupteinstellungen",
"mainStat": "Hauptwert",
"set": "Set",
"pleaseSelectASet": "Bitte wähle ein Set",
"effectBonus": "Effektbonus",
"totalRoll": "Gesamte Aufwertungen",
"randomizeStats": "Werte zufällig",
"randomizeRolls": "Aufwertungen zufällig",
"selectASubStat": "Wähle einen Nebenwert",
"selectASet": "Wähle ein Set",
"selectAMainStat": "Wähle einen Hauptwert",
"save": "Speichern",
"reset": "Zurücksetzen",
"roll": "Aufwertung",
"step": "Schritt",
"memoryOfChaos": "Vergessene Halle",
"pureFiction": "Reine Fiktion",
"apocalypticShadow": "Apokalyptischer Schatten",
"customEnemy": "Benutzerdefinierter Feind",
"simulatedUniverse": "Universum-Simulation",
"floor": "Ebene",
"side": "Seite",
"wave": "Welle",
"stage": "Phase",
"useCycleCount": "Zyklen-Zählung verwenden?",
"useTurbulenceBuff": "Turbulenz-Buff verwenden?",
"firstHalfEnemies": "Gegner erste Hälfte",
"secondHalfEnemies": "Gegner zweite Hälfte",
"firstNodeEnemies": "Gegner Knoten 1",
"secondNodeEnemies": "Gegner Knoten 2",
"thirdNodeEnemies": "Gegner Knoten 3",
"firstNode": "Knoten 1",
"secondNode": "Knoten 2",
"thirdNode": "Knoten 3",
"listEnemies": "Gegnerliste",
"turbulenceBuff": "Turbulenz-Buff",
"noEventSelected": "Kein Ereignis ausgewählt",
"noTurbulenceBuff": "Kein Turbulenz-Buff",
"upper": "Oben",
"lower": "Unten",
"upperToLower": "Oben -> Unten",
"lowerToUpper": "Unten -> Oben",
"selectMOCEvent": "MOC-Ereignis wählen",
"selectPFEvent": "PF-Ereignis wählen",
"selectASEvent": "AS-Ereignis wählen",
"selectCEEvent": "CE-Ereignis wählen",
"selectEvent": "Ereignis wählen",
"selectFloor": "Ebene wählen",
"selectSide": "Seite wählen",
"selectBuff": "Buff wählen",
"selectStage": "Phase wählen",
"previous": "Zurück",
"next": "Weiter",
"noMonstersFound": "Keine Monster gefunden",
"addNewWave": "Neue Welle hinzufügen",
"searchStage": "Phase suchen...",
"noStageFound": "Keine Phase gefunden",
"searchMonster": "Monster suchen...",
"changeRelic": "Relikt wechseln",
"deleteRelic": "Relikt löschen",
"deleteRelicConfirm": "Möchtest du das Relikt auf diesem Platz wirklich löschen?",
"setEffects": "Effekte einstellen",
"details": "Details",
"normal": "Standardangriff",
"bpskill": "Fähigkeit",
"maze": "Technik",
"ultra": "Ultimate",
"servantskill": "Memosprite-Fähigkeit",
"severaltalent": "Memosprite-Talent",
"singleattack": "Einzelangriff",
"enhance": "Verbessern",
"summon": "Beschwören",
"mazeattack": "Technik-Angriff",
"blast": "Explosion",
"restore": "Wiederherstellen",
"support": "Unterstützung",
"aoeattack": "Flächenangriff",
"impair": "Beeinträchtigen",
"bounce": "Abprallen",
"active": "Aktiv",
"defence": "Verteidigung",
"inactive": "Inaktiv",
"maxAll": "Alles maximieren",
"maxAllSuccess": "Fähigkeiten erfolgreich maximiert.",
"maxAllFailed": "Fehler beim Maximieren.",
"noRelicEquipped": "Kein Relikt ausgerüstet",
"anomalyArbitration": "Anomalie-Schiedsgericht",
"normalMode": "Normaler Modus",
"hardMode": "Schwerer Modus",
"selectPEAKEvent": "PEAK-Ereignis wählen",
"mode": "Modus",
"selectMode": "Modus wählen",
"rollBack": "Rückgängig",
"upRoll": "Aufwertung hoch",
"downRoll": "Aufwertung runter",
"actions": "Aktionen",
"avatars": "Avatare",
"quickView": "Schnellansicht",
"extraSetting": "Extra-Einstellungen",
"disableCensorship": "Zensur deaktivieren",
"hideUI": "UI verstecken",
"theoryCraftMode": "Theorycraft-Modus",
"cycleCount": "Zyklen-Anzahl",
"pleaseSelectAllSubStats": "Bitte alle Nebenwerte auswählen",
"subStatRollCountCannotBeZero": "Nebenwert-Aufwertungen dürfen nicht null sein",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Mehrpfad-Charakter",
"mainPath": "Hauptpfad",
"march7Path": "Pfad 7. März",
"challenge": "Herausforderung",
"skipNode": "Knoten überspringen",
"disableSkip": "Überspringen deaktivieren",
"skipNode1": "Knoten 1 überspringen",
"skipNode2": "Knoten 2 überspringen",
"extraFeatures": "Zusatzfunktionen",
"detailTheoryCraft": "Ermöglicht die Anpassung der Zykluszahl und der Gegner-LP.",
"detailSkipNode": "Ermöglicht das Überspringen (Knoten 1/2) im Memory of Chaos oder Pure Fiction.",
"detailChallengePeak": "Ändert die Peak-Saison in der aktuellen Anomalie.",
"detailHiddenUi": "Versteckt die Spiel-Benutzeroberfläche.",
"detailDisableCensorship": "Deaktiviert die Zensur im Spiel.",
"detailMultipathCharacter": "Ermöglicht das Ändern des Pfades bestimmter Charaktere.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Liste Zusatzeffekte",
"extra": "Extra",
"customLineup": "Benutzerdefinierte Aufstellung"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Skill Type",
"skillName": "Skill Name",
"character": "Character",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Element",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Type",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Thunder",
"wind": "Wind",
"hp": "Hp",
"atk": "Atk",
"speed": "Speed",
"critRate": "Crit Rate",
"critDmg": "Crit Dmg",
"breakEffect": "Break Effect",
"effectRes": "Effect Res",
"energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire damage boost",
"iceDmgBoost": "Ice damage Boost",
"imaginaryDmgBoost": "Imaginary damage boost",
"physicalDmgBoost": "Physical damage boost",
"quantumDmgBoost": "Quantum damage boost",
"thunderDmgBoost": "Thunder damage boost",
"windDmgBoost": "Wind damage boost",
"pursued": "Additional damage",
"true damage": "True damage",
"elationdamage": "Elation damage",
"follow-up": "Follow-up Damage",
"elemental damage": "Break and Super break damage",
"dot": "Damage over time ",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relics",
"eidolons": "Eidolons",
"lightcones": "Lightcones",
"loadData": "Load data",
"exportData": "Export data",
"connectSetting": "Connection Setting",
"connected": "Connected",
"unconnected": "Unconnected",
"psConnection": "PS Connection",
"connectionType": "Connection Type",
"status": "Status",
"connectPs": "Connect PS",
"disconnect": "Disconnect",
"other": "Other",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Monster Setting",
"serverUrl": "Server URL",
"privateType": "Private Type",
"local": "Local",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Enter server URL",
"placeholderUsername": "Enter username",
"placeholderPassword": "Enter password",
"connectedSuccess": "Connected to PS successfully",
"connectedFailed": "Failed to connect to PS",
"syncSuccess": "Synced data to PS successfully",
"syncFailed": "Failed to sync data to PS",
"sync": "Sync",
"importSetting": "Import Setting",
"profile": "Profile",
"default": "Default",
"copyProfiles": "Copy profiles",
"addNewProfile": "Add new profile",
"createNewProfile": "Create new profile",
"editProfile": "Edit profile",
"placeholderProfileName": "Enter profile name",
"profileName": "Profile name",
"create": "Create",
"update": "Update",
"characterInformation": "Character Information",
"skills": "Skills",
"showcaseCard": "Showcase Card",
"comingSoon": "Coming soon",
"characterName": "Character name",
"placeholderCharacter": "Enter character name",
"characterSettings": "Character Settings",
"levelConfiguration": "Level Configuration",
"characterLevel": "Character Level",
"max": "MAX",
"ultimateEnergy": "Ultimate Energy",
"currentEnergy": "Current Energy",
"setTo50": "Set to 50%",
"battleConfiguration": "Battle Configuration",
"useTechnique": "Use Technique",
"techniqueNote": "Enable pre-battle technique effects",
"enhancement": "Enhancement",
"enhancementLevel": "Enhancement Level",
"origin": "Origin",
"enhancedNote": "Higher enhanced unlock additional abilities",
"lightconeEquipment": "Lightcone Equipment",
"lightconeSettings": "Lightcone Settings",
"placeholderLevel": "Enter level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Higher ranks provide stronger effects",
"changeLightcone": "Change Lightcone",
"removeLightcone": "Remove Lightcone",
"equipLightcone": "Equip Lightcone",
"noLightconeEquipped": "No Lightcone Equipped",
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
"filter": "Filter",
"selectedCharacters": "Selected Characters",
"selectedProfiles": "Selected Profiles",
"clearAll": "Clear All",
"selectAll": "Select All",
"copy": "Copy",
"copied": "Copied",
"noAvatarSelected": "No avatar selected",
"noAvatarToCopySelected": "No avatar to copy selected",
"pleaseSelectAtLeastOneProfile": "Please select at least one profile",
"pleaseEnterUid": "Please enter UID",
"failedToFetchEnkaData": "Failed to fetch enka data",
"pleaseSelectAtLeastOneCharacter": "Please select at least one character",
"noDataToImport": "No data to import",
"pleaseSelectAFile": "Please select a file",
"fileMustBeAValidJsonFile": "File must be a valid json file",
"importEnkaDataSuccess": "Import Enka data success",
"importFreeSRDataSuccess": "Import FreeSR data success",
"importDatabaseSuccess": "Import database success",
"getData": "Get Data",
"import": "Import",
"freeSRImport": "FreeSR Import",
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
"pickAFile": "Pick a file",
"lightConeSetting": "LightCone Setting",
"relicMaker": "Relic Maker",
"pleaseSelectAllOptions": "Please select all options",
"relicSavedSuccessfully": "Relic saved successfully",
"mainSettings": "Main Settings",
"mainStat": "Main Stat",
"set": "Set",
"pleaseSelectASet": "Please select a set",
"effectBonus": "Effect Bonus",
"totalRoll": "Total Roll",
"randomizeStats": "Randomize Stats",
"randomizeRolls": "Randomize Rolls",
"selectASubStat": "Select a sub stat",
"selectASet": "Select a set",
"selectAMainStat": "Select a main stat",
"save": "Save",
"reset": "Reset",
"roll": "Roll",
"step": "Step",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Custom Enemy",
"simulatedUniverse": "Simulated Universe",
"floor": "Floor",
"side": "Side",
"wave": "Wave",
"stage": "Stage",
"useCycleCount": "Use cycle count?",
"useTurbulenceBuff": "Use turbulence buff?",
"firstHalfEnemies": "First half enemies",
"secondHalfEnemies": "Second half enemies",
"firstNodeEnemies": "First node enemies",
"secondNodeEnemies": "Second node enemies",
"thirdNodeEnemies": "Third node enemies",
"firstNode": "First Node",
"secondNode": "Second Node",
"thirdNode": "Third Node",
"listEnemies": "List enemies",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "No event selected",
"noTurbulenceBuff": "No Turbulence Buff",
"upper": "Upper",
"lower": "Lower",
"upperToLower": "Upper -> Lower",
"lowerToUpper": "Lower -> Upper",
"selectMOCEvent": "Select MOC Event",
"selectPFEvent": "Select PF Event",
"selectASEvent": "Select AS Event",
"selectCEEvent": "Select CE Event",
"selectEvent": "Select Event",
"selectFloor": "Select a Floor",
"selectSide": "Select a Side",
"selectBuff": "Select a Buff",
"selectStage": "Select a Stage",
"previous": "Previous",
"next": "Next",
"noMonstersFound": "No monsters found",
"addNewWave": "Add New Wave",
"searchStage": "Search stage...",
"noStageFound": "No stage found",
"searchMonster": "Search monster...",
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects",
"details": "Details",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"servantskill": "Memosprite Skill",
"severaltalent": "Memosprite Talent",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Active",
"defence": "Defence",
"inactive": "Inactive",
"maxAll": "Max All",
"maxAllSuccess": "Successfully set skill level to max.",
"maxAllFailed": "Failed to set skill level to max.",
"noRelicEquipped": "No relic equipped",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Normal Mode",
"hardMode": "Hard Mode",
"selectPEAKEvent": "Select PEAK Event",
"mode": "Mode",
"selectMode": "Select a mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Quick View",
"extraSetting": "Extra Settings",
"disableCensorship": "Disable Censorship",
"hideUI": "Hide UI",
"theoryCraftMode": "Theorycraft Mode",
"cycleCount": "Cycle Count",
"pleaseSelectAllSubStats": "Please select all sub stats",
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Multipath Character",
"mainPath": "Main Path",
"march7Path": "March 7 Path",
"challenge": "Challenge",
"skipNode": "Skip Node",
"disableSkip": "Disable skip",
"skipNode1": "Skip node 1",
"skipNode2": "Skip node 2",
"extraFeatures": "Extra Features",
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
"detailHiddenUi": "Enabling this feature will hide the game UI.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"detailMultipathCharacter": "Allows changing the Path of certain characters.",
"trailblazer": "Trailblazer",
"listExtraEffect": "List Extra Effect",
"extra": "Extra",
"customLineup": "Custom Lineup"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Skill Type",
"skillName": "Skill Name",
"character": "Character",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Element",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Type",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Thunder",
"wind": "Wind",
"hp": "Hp",
"atk": "Atk",
"speed": "Speed",
"critRate": "Crit Rate",
"critDmg": "Crit Dmg",
"breakEffect": "Break Effect",
"effectRes": "Effect Res",
"energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire damage boost",
"iceDmgBoost": "Ice damage Boost",
"imaginaryDmgBoost": "Imaginary damage boost",
"physicalDmgBoost": "Physical damage boost",
"quantumDmgBoost": "Quantum damage boost",
"thunderDmgBoost": "Thunder damage boost",
"windDmgBoost": "Wind damage boost",
"pursued": "Additional damage",
"true damage": "True damage",
"elationdamage": "Elation damage",
"follow-up": "Follow-up Damage",
"elemental damage": "Break and Super break damage",
"dot": "Damage over time ",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relics",
"eidolons": "Eidolons",
"lightcones": "Lightcones",
"loadData": "Load data",
"exportData": "Export data",
"connectSetting": "Connection Setting",
"connected": "Connected",
"unconnected": "Unconnected",
"psConnection": "PS Connection",
"connectionType": "Connection Type",
"status": "Status",
"connectPs": "Connect PS",
"disconnect": "Disconnect",
"other": "Other",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Monster Setting",
"serverUrl": "Server URL",
"privateType": "Private Type",
"local": "Local",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Enter server URL",
"placeholderUsername": "Enter username",
"placeholderPassword": "Enter password",
"connectedSuccess": "Connected to PS successfully",
"connectedFailed": "Failed to connect to PS",
"syncSuccess": "Synced data to PS successfully",
"syncFailed": "Failed to sync data to PS",
"sync": "Sync",
"importSetting": "Import Setting",
"profile": "Profile",
"default": "Default",
"copyProfiles": "Copy profiles",
"addNewProfile": "Add new profile",
"createNewProfile": "Create new profile",
"editProfile": "Edit profile",
"placeholderProfileName": "Enter profile name",
"profileName": "Profile name",
"create": "Create",
"update": "Update",
"characterInformation": "Character Information",
"skills": "Skills",
"showcaseCard": "Showcase Card",
"comingSoon": "Coming soon",
"characterName": "Character name",
"placeholderCharacter": "Enter character name",
"characterSettings": "Character Settings",
"levelConfiguration": "Level Configuration",
"characterLevel": "Character Level",
"max": "MAX",
"ultimateEnergy": "Ultimate Energy",
"currentEnergy": "Current Energy",
"setTo50": "Set to 50%",
"battleConfiguration": "Battle Configuration",
"useTechnique": "Use Technique",
"techniqueNote": "Enable pre-battle technique effects",
"enhancement": "Enhancement",
"enhancementLevel": "Enhancement Level",
"origin": "Origin",
"enhancedNote": "Higher enhanced unlock additional abilities",
"lightconeEquipment": "Lightcone Equipment",
"lightconeSettings": "Lightcone Settings",
"placeholderLevel": "Enter level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Higher ranks provide stronger effects",
"changeLightcone": "Change Lightcone",
"removeLightcone": "Remove Lightcone",
"equipLightcone": "Equip Lightcone",
"noLightconeEquipped": "No Lightcone Equipped",
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
"filter": "Filter",
"selectedCharacters": "Selected Characters",
"selectedProfiles": "Selected Profiles",
"clearAll": "Clear All",
"selectAll": "Select All",
"copy": "Copy",
"copied": "Copied",
"noAvatarSelected": "No avatar selected",
"noAvatarToCopySelected": "No avatar to copy selected",
"pleaseSelectAtLeastOneProfile": "Please select at least one profile",
"pleaseEnterUid": "Please enter UID",
"failedToFetchEnkaData": "Failed to fetch enka data",
"pleaseSelectAtLeastOneCharacter": "Please select at least one character",
"noDataToImport": "No data to import",
"pleaseSelectAFile": "Please select a file",
"fileMustBeAValidJsonFile": "File must be a valid json file",
"importEnkaDataSuccess": "Import Enka data success",
"importFreeSRDataSuccess": "Import FreeSR data success",
"importDatabaseSuccess": "Import database success",
"getData": "Get Data",
"import": "Import",
"freeSRImport": "FreeSR Import",
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
"pickAFile": "Pick a file",
"lightConeSetting": "LightCone Setting",
"relicMaker": "Relic Maker",
"pleaseSelectAllOptions": "Please select all options",
"relicSavedSuccessfully": "Relic saved successfully",
"mainSettings": "Main Settings",
"mainStat": "Main Stat",
"set": "Set",
"pleaseSelectASet": "Please select a set",
"effectBonus": "Effect Bonus",
"totalRoll": "Total Roll",
"randomizeStats": "Randomize Stats",
"randomizeRolls": "Randomize Rolls",
"selectASubStat": "Select a sub stat",
"selectASet": "Select a set",
"selectAMainStat": "Select a main stat",
"save": "Save",
"reset": "Reset",
"roll": "Roll",
"step": "Step",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Custom Enemy",
"simulatedUniverse": "Simulated Universe",
"floor": "Floor",
"side": "Side",
"wave": "Wave",
"stage": "Stage",
"useCycleCount": "Use cycle count?",
"useTurbulenceBuff": "Use turbulence buff?",
"firstHalfEnemies": "First half enemies",
"secondHalfEnemies": "Second half enemies",
"firstNodeEnemies": "First node enemies",
"secondNodeEnemies": "Second node enemies",
"thirdNodeEnemies": "Third node enemies",
"firstNode": "First Node",
"secondNode": "Second Node",
"thirdNode": "Third Node",
"listEnemies": "List enemies",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "No event selected",
"noTurbulenceBuff": "No Turbulence Buff",
"upper": "Upper",
"lower": "Lower",
"upperToLower": "Upper -> Lower",
"lowerToUpper": "Lower -> Upper",
"selectMOCEvent": "Select MOC Event",
"selectPFEvent": "Select PF Event",
"selectASEvent": "Select AS Event",
"selectCEEvent": "Select CE Event",
"selectEvent": "Select Event",
"selectFloor": "Select a Floor",
"selectSide": "Select a Side",
"selectBuff": "Select a Buff",
"selectStage": "Select a Stage",
"previous": "Previous",
"next": "Next",
"noMonstersFound": "No monsters found",
"addNewWave": "Add New Wave",
"searchStage": "Search stage...",
"noStageFound": "No stage found",
"searchMonster": "Search monster...",
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects",
"details": "Details",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"servantskill": "Memosprite Skill",
"severaltalent": "Memosprite Talent",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Active",
"defence": "Defence",
"inactive": "Inactive",
"maxAll": "Max All",
"maxAllSuccess": "Successfully set skill level to max.",
"maxAllFailed": "Failed to set skill level to max.",
"noRelicEquipped": "No relic equipped",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Normal Mode",
"hardMode": "Hard Mode",
"selectPEAKEvent": "Select PEAK Event",
"mode": "Mode",
"selectMode": "Select a mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Quick View",
"extraSetting": "Extra Settings",
"disableCensorship": "Disable Censorship",
"hideUI": "Hide UI",
"theoryCraftMode": "Theorycraft Mode",
"cycleCount": "Cycle Count",
"pleaseSelectAllSubStats": "Please select all sub stats",
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Multipath Character",
"mainPath": "Main Path",
"march7Path": "March 7 Path",
"challenge": "Challenge",
"skipNode": "Skip Node",
"disableSkip": "Disable skip",
"skipNode1": "Skip node 1",
"skipNode2": "Skip node 2",
"extraFeatures": "Extra Features",
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
"detailHiddenUi": "Enabling this feature will hide the game UI.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"detailMultipathCharacter": "Allows changing the Path of certain characters.",
"trailblazer": "Trailblazer",
"listExtraEffect": "List Extra Effect",
"extra": "Extra",
"customLineup": "Custom Lineup"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Herramientas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de habilidad",
"skillName": "Nombre de la habilidad",
"character": "Personaje",
"id": "Id",
"path": "Vía",
"rarity": "Rareza",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Habilidad",
"ultimate": "Habilidad Definitiva",
"servant": "Mnemoduende",
"damage": "Daño",
"type": "Tipo",
"warrior": "La Destrucción",
"knight": "La Conservación",
"mage": "La Erudición",
"priest": "La Abundancia",
"rogue": "La Cacería",
"shaman": "La Armonía",
"warlock": "La Nihilidad",
"memory": "La Reminiscencia",
"elation": "La Exultación",
"fire": "Fuego",
"ice": "Hielo",
"imaginary": "Imaginario",
"physical": "Físico",
"quantum": "Cuántico",
"thunder": "Rayo",
"wind": "Viento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Prob. CRIT",
"critDmg": "Daño CRIT",
"breakEffect": "Efecto de Ruptura",
"effectRes": "RES a Efecto",
"energyRegenerationRate": "Recuperación de Energía",
"effectHitRate": "Acierto de Efecto",
"outgoingHealingBoost": "Bono de Curación",
"fireDmgBoost": "Aumento de Daño de Fuego",
"iceDmgBoost": "Aumento de Daño de Hielo",
"imaginaryDmgBoost": "Aumento de Daño Imaginario",
"physicalDmgBoost": "Aumento de Daño Físico",
"quantumDmgBoost": "Aumento de Daño Cuántico",
"thunderDmgBoost": "Aumento de Daño de Rayo",
"windDmgBoost": "Aumento de Daño de Viento",
"pursued": "Daño adicional",
"true damage": "Daño verdadero",
"elationdamage": "Daño de Exultación",
"follow-up": "Daño de Ataque Adicional",
"elemental damage": "Daño de Ruptura y Superruptura",
"dot": "Daño con el tiempo",
"qte": "Habilidad QTE",
"level": "Nivel",
"relics": "Artefactos",
"eidolons": "Eidolones",
"lightcones": "Conos de Luz",
"loadData": "Cargar datos",
"exportData": "Exportar datos",
"connectSetting": "Ajustes de conexión",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexión PS",
"connectionType": "Tipo de conexión",
"status": "Estado",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Otro",
"freeSr": "FreeSR",
"database": "Base de datos",
"enka": "Enka",
"monsterSetting": "Ajuste de Monstruos",
"serverUrl": "URL del Servidor",
"privateType": "Tipo privado",
"local": "Local",
"server": "Servidor",
"username": "Usuario",
"password": "Contraseña",
"placeholderServerUrl": "Introducir URL del servidor",
"placeholderUsername": "Introducir usuario",
"placeholderPassword": "Introducir contraseña",
"connectedSuccess": "Conectado a PS exitosamente",
"connectedFailed": "Fallo al conectar a PS",
"syncSuccess": "Datos sincronizados a PS exitosamente",
"syncFailed": "Fallo al sincronizar datos a PS",
"sync": "Sincronizar",
"importSetting": "Ajustes de Importación",
"profile": "Perfil",
"default": "Por defecto",
"copyProfiles": "Copiar perfiles",
"addNewProfile": "Añadir nuevo perfil",
"createNewProfile": "Crear nuevo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Introducir nombre de perfil",
"profileName": "Nombre del perfil",
"create": "Crear",
"update": "Actualizar",
"characterInformation": "Información del personaje",
"skills": "Habilidades",
"showcaseCard": "Tarjeta de presentación del personaje",
"comingSoon": "Próximamente",
"characterName": "Nombre del personaje",
"placeholderCharacter": "Introducir nombre del personaje",
"characterSettings": "Ajustes del Personaje",
"levelConfiguration": "Configuración de Nivel",
"characterLevel": "Nivel de Personaje",
"max": "MÁX",
"ultimateEnergy": "Energía de Habilidad Definitiva",
"currentEnergy": "Energía al inicio del combate",
"setTo50": "Ajustar al 50%",
"battleConfiguration": "Configuración de Batalla",
"useTechnique": "Usar Técnica",
"techniqueNote": "Activar efectos de técnica pre-batalla",
"enhancement": "Mejora",
"enhancementLevel": "Nivel de Mejora",
"origin": "Original",
"enhancedNote": "Niveles superiores desbloquean más habilidades",
"lightconeEquipment": "Equipamiento de Cono de Luz",
"lightconeSettings": "Ajustes de Cono de Luz",
"placeholderLevel": "Introducir nivel",
"superimpositionRank": "Rango de Superposición",
"ranksNote": "Rangos mayores ofrecen efectos más fuertes",
"changeLightcone": "Cambiar Cono de Luz",
"removeLightcone": "Quitar Cono de Luz",
"equipLightcone": "Equipar Cono de Luz",
"noLightconeEquipped": "Sin Cono de Luz",
"equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje",
"filter": "Filtro",
"selectedCharacters": "Personajes Seleccionados",
"selectedProfiles": "Perfiles Seleccionados",
"clearAll": "Limpiar Todo",
"selectAll": "Seleccionar Todo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Ningún personaje seleccionado",
"noAvatarToCopySelected": "Ningún personaje seleccionado para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil",
"pleaseEnterUid": "Por favor, introduce UID",
"failedToFetchEnkaData": "Fallo al obtener datos de Enka",
"pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje",
"noDataToImport": "Sin datos para importar",
"pleaseSelectAFile": "Por favor, selecciona un archivo",
"fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido",
"importEnkaDataSuccess": "Datos de Enka importados con éxito",
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
"importDatabaseSuccess": "Base de datos importada con éxito",
"getData": "Obtener Datos",
"import": "Importar",
"freeSRImport": "Importar FreeSR",
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
"pickAFile": "Elegir un archivo",
"lightConeSetting": "Ajuste de Cono de Luz",
"relicMaker": "Creador de Artefactos",
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
"relicSavedSuccessfully": "Artefacto guardado exitosamente",
"mainSettings": "Ajustes Principales",
"mainStat": "Estadística Principal",
"set": "Conjunto",
"pleaseSelectASet": "Por favor, selecciona un conjunto",
"effectBonus": "Bono de Efecto",
"totalRoll": "Total de Mejoras",
"randomizeStats": "Estadísticas Aleatorias",
"randomizeRolls": "Mejoras Aleatorias",
"selectASubStat": "Seleccionar estadística secundaria",
"selectASet": "Seleccionar conjunto",
"selectAMainStat": "Seleccionar estadística principal",
"save": "Guardar",
"reset": "Reiniciar",
"roll": "Mejora",
"step": "Paso",
"memoryOfChaos": "Memoria del Caos",
"pureFiction": "Pura Ficción",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Enemigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Piso",
"side": "Lado",
"wave": "Oleada",
"stage": "Etapa",
"useCycleCount": "¿Usar conteo de ciclos?",
"useTurbulenceBuff": "¿Usar buff de turbulencia?",
"firstHalfEnemies": "Enemigos primera mitad",
"secondHalfEnemies": "Enemigos segunda mitad",
"firstNodeEnemies": "Enemigos del nodo 1",
"secondNodeEnemies": "Enemigos del nodo 2",
"thirdNodeEnemies": "Enemigos del nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de enemigos",
"turbulenceBuff": "Buff de Turbulencia",
"noEventSelected": "Ningún evento seleccionado",
"noTurbulenceBuff": "Sin Buff de Turbulencia",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Seleccionar evento de MOC",
"selectPFEvent": "Seleccionar evento de PF",
"selectASEvent": "Seleccionar evento de AS",
"selectCEEvent": "Seleccionar evento de CE",
"selectEvent": "Seleccionar Evento",
"selectFloor": "Seleccionar Piso",
"selectSide": "Seleccionar Lado",
"selectBuff": "Seleccionar Buff",
"selectStage": "Seleccionar Etapa",
"previous": "Anterior",
"next": "Siguiente",
"noMonstersFound": "No se encontraron monstruos",
"addNewWave": "Añadir Nueva Oleada",
"searchStage": "Buscar etapa...",
"noStageFound": "No se encontró etapa",
"searchMonster": "Buscar monstruo...",
"changeRelic": "Cambiar artefacto",
"deleteRelic": "Borrar artefacto",
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
"setEffects": "Configurar Efectos",
"details": "Detalles",
"normal": "Ataque Básico",
"bpskill": "Habilidad",
"maze": "Técnica",
"ultra": "Habilidad Definitiva",
"servantskill": "Habilidad de Mnemoduende",
"severaltalent": "Talento de Mnemoduende",
"singleattack": "Ataque Individual",
"enhance": "Mejorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Ráfaga",
"restore": "Restaurar",
"support": "Soporte",
"aoeattack": "Ataque en Área",
"impair": "Debilitación",
"bounce": "Rebote",
"active": "Activo",
"defence": "Defensa",
"inactive": "Inactivo",
"maxAll": "Maximizar Todo",
"maxAllSuccess": "Habilidades maximizadas con éxito.",
"maxAllFailed": "Fallo al maximizar habilidades.",
"noRelicEquipped": "Sin artefacto equipado",
"anomalyArbitration": "Arbitraje Atípico",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Seleccionar evento PEAK",
"mode": "Modo",
"selectMode": "Seleccionar un modo",
"rollBack": "Retroceder",
"upRoll": "Subir Mejora",
"downRoll": "Bajar Mejora",
"actions": "Acciones",
"avatars": "Avatares",
"quickView": "Vista Rápida",
"extraSetting": "Ajustes Extra",
"disableCensorship": "Desactivar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Conteo de Ciclos",
"pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas",
"subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personaje Multivía",
"mainPath": "Vía Principal",
"march7Path": "Vía de Siete de Marzo",
"challenge": "Desafío",
"skipNode": "Saltar Nodo",
"disableSkip": "Desactivar salto",
"skipNode1": "Saltar nodo 1",
"skipNode2": "Saltar nodo 2",
"extraFeatures": "Características Extra",
"detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.",
"detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.",
"detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.",
"detailHiddenUi": "Oculta la interfaz del juego.",
"detailDisableCensorship": "Desactiva la censura dentro del juego.",
"detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.",
"trailblazer": "Trazacaminos",
"listExtraEffect": "Lista de Efectos Extra",
"extra": "Extra",
"customLineup": "Alineación de equipo personalizada"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Herramientas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de habilidad",
"skillName": "Nombre de la habilidad",
"character": "Personaje",
"id": "Id",
"path": "Vía",
"rarity": "Rareza",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Habilidad",
"ultimate": "Habilidad Definitiva",
"servant": "Mnemoduende",
"damage": "Daño",
"type": "Tipo",
"warrior": "La Destrucción",
"knight": "La Conservación",
"mage": "La Erudición",
"priest": "La Abundancia",
"rogue": "La Cacería",
"shaman": "La Armonía",
"warlock": "La Nihilidad",
"memory": "La Reminiscencia",
"elation": "La Exultación",
"fire": "Fuego",
"ice": "Hielo",
"imaginary": "Imaginario",
"physical": "Físico",
"quantum": "Cuántico",
"thunder": "Rayo",
"wind": "Viento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Prob. CRIT",
"critDmg": "Daño CRIT",
"breakEffect": "Efecto de Ruptura",
"effectRes": "RES a Efecto",
"energyRegenerationRate": "Recuperación de Energía",
"effectHitRate": "Acierto de Efecto",
"outgoingHealingBoost": "Bono de Curación",
"fireDmgBoost": "Aumento de Daño de Fuego",
"iceDmgBoost": "Aumento de Daño de Hielo",
"imaginaryDmgBoost": "Aumento de Daño Imaginario",
"physicalDmgBoost": "Aumento de Daño Físico",
"quantumDmgBoost": "Aumento de Daño Cuántico",
"thunderDmgBoost": "Aumento de Daño de Rayo",
"windDmgBoost": "Aumento de Daño de Viento",
"pursued": "Daño adicional",
"true damage": "Daño verdadero",
"elationdamage": "Daño de Exultación",
"follow-up": "Daño de Ataque Adicional",
"elemental damage": "Daño de Ruptura y Superruptura",
"dot": "Daño con el tiempo",
"qte": "Habilidad QTE",
"level": "Nivel",
"relics": "Artefactos",
"eidolons": "Eidolones",
"lightcones": "Conos de Luz",
"loadData": "Cargar datos",
"exportData": "Exportar datos",
"connectSetting": "Ajustes de conexión",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexión PS",
"connectionType": "Tipo de conexión",
"status": "Estado",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Otro",
"freeSr": "FreeSR",
"database": "Base de datos",
"enka": "Enka",
"monsterSetting": "Ajuste de Monstruos",
"serverUrl": "URL del Servidor",
"privateType": "Tipo privado",
"local": "Local",
"server": "Servidor",
"username": "Usuario",
"password": "Contraseña",
"placeholderServerUrl": "Introducir URL del servidor",
"placeholderUsername": "Introducir usuario",
"placeholderPassword": "Introducir contraseña",
"connectedSuccess": "Conectado a PS exitosamente",
"connectedFailed": "Fallo al conectar a PS",
"syncSuccess": "Datos sincronizados a PS exitosamente",
"syncFailed": "Fallo al sincronizar datos a PS",
"sync": "Sincronizar",
"importSetting": "Ajustes de Importación",
"profile": "Perfil",
"default": "Por defecto",
"copyProfiles": "Copiar perfiles",
"addNewProfile": "Añadir nuevo perfil",
"createNewProfile": "Crear nuevo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Introducir nombre de perfil",
"profileName": "Nombre del perfil",
"create": "Crear",
"update": "Actualizar",
"characterInformation": "Información del personaje",
"skills": "Habilidades",
"showcaseCard": "Tarjeta de presentación del personaje",
"comingSoon": "Próximamente",
"characterName": "Nombre del personaje",
"placeholderCharacter": "Introducir nombre del personaje",
"characterSettings": "Ajustes del Personaje",
"levelConfiguration": "Configuración de Nivel",
"characterLevel": "Nivel de Personaje",
"max": "MÁX",
"ultimateEnergy": "Energía de Habilidad Definitiva",
"currentEnergy": "Energía al inicio del combate",
"setTo50": "Ajustar al 50%",
"battleConfiguration": "Configuración de Batalla",
"useTechnique": "Usar Técnica",
"techniqueNote": "Activar efectos de técnica pre-batalla",
"enhancement": "Mejora",
"enhancementLevel": "Nivel de Mejora",
"origin": "Original",
"enhancedNote": "Niveles superiores desbloquean más habilidades",
"lightconeEquipment": "Equipamiento de Cono de Luz",
"lightconeSettings": "Ajustes de Cono de Luz",
"placeholderLevel": "Introducir nivel",
"superimpositionRank": "Rango de Superposición",
"ranksNote": "Rangos mayores ofrecen efectos más fuertes",
"changeLightcone": "Cambiar Cono de Luz",
"removeLightcone": "Quitar Cono de Luz",
"equipLightcone": "Equipar Cono de Luz",
"noLightconeEquipped": "Sin Cono de Luz",
"equipLightconeNote": "Equipa un cono de luz para mejorar tu personaje",
"filter": "Filtro",
"selectedCharacters": "Personajes Seleccionados",
"selectedProfiles": "Perfiles Seleccionados",
"clearAll": "Limpiar Todo",
"selectAll": "Seleccionar Todo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Ningún personaje seleccionado",
"noAvatarToCopySelected": "Ningún personaje seleccionado para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecciona al menos un perfil",
"pleaseEnterUid": "Por favor, introduce UID",
"failedToFetchEnkaData": "Fallo al obtener datos de Enka",
"pleaseSelectAtLeastOneCharacter": "Por favor, selecciona al menos un personaje",
"noDataToImport": "Sin datos para importar",
"pleaseSelectAFile": "Por favor, selecciona un archivo",
"fileMustBeAValidJsonFile": "El archivo debe ser un JSON válido",
"importEnkaDataSuccess": "Datos de Enka importados con éxito",
"importFreeSRDataSuccess": "Datos de FreeSR importados con éxito",
"importDatabaseSuccess": "Base de datos importada con éxito",
"getData": "Obtener Datos",
"import": "Importar",
"freeSRImport": "Importar FreeSR",
"onlySupportFreeSRJsonFile": "Solo soporta archivos JSON de FreeSR",
"pickAFile": "Elegir un archivo",
"lightConeSetting": "Ajuste de Cono de Luz",
"relicMaker": "Creador de Artefactos",
"pleaseSelectAllOptions": "Por favor selecciona todas las opciones",
"relicSavedSuccessfully": "Artefacto guardado exitosamente",
"mainSettings": "Ajustes Principales",
"mainStat": "Estadística Principal",
"set": "Conjunto",
"pleaseSelectASet": "Por favor, selecciona un conjunto",
"effectBonus": "Bono de Efecto",
"totalRoll": "Total de Mejoras",
"randomizeStats": "Estadísticas Aleatorias",
"randomizeRolls": "Mejoras Aleatorias",
"selectASubStat": "Seleccionar estadística secundaria",
"selectASet": "Seleccionar conjunto",
"selectAMainStat": "Seleccionar estadística principal",
"save": "Guardar",
"reset": "Reiniciar",
"roll": "Mejora",
"step": "Paso",
"memoryOfChaos": "Memoria del Caos",
"pureFiction": "Pura Ficción",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Enemigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Piso",
"side": "Lado",
"wave": "Oleada",
"stage": "Etapa",
"useCycleCount": "¿Usar conteo de ciclos?",
"useTurbulenceBuff": "¿Usar buff de turbulencia?",
"firstHalfEnemies": "Enemigos primera mitad",
"secondHalfEnemies": "Enemigos segunda mitad",
"firstNodeEnemies": "Enemigos del nodo 1",
"secondNodeEnemies": "Enemigos del nodo 2",
"thirdNodeEnemies": "Enemigos del nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de enemigos",
"turbulenceBuff": "Buff de Turbulencia",
"noEventSelected": "Ningún evento seleccionado",
"noTurbulenceBuff": "Sin Buff de Turbulencia",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Seleccionar evento de MOC",
"selectPFEvent": "Seleccionar evento de PF",
"selectASEvent": "Seleccionar evento de AS",
"selectCEEvent": "Seleccionar evento de CE",
"selectEvent": "Seleccionar Evento",
"selectFloor": "Seleccionar Piso",
"selectSide": "Seleccionar Lado",
"selectBuff": "Seleccionar Buff",
"selectStage": "Seleccionar Etapa",
"previous": "Anterior",
"next": "Siguiente",
"noMonstersFound": "No se encontraron monstruos",
"addNewWave": "Añadir Nueva Oleada",
"searchStage": "Buscar etapa...",
"noStageFound": "No se encontró etapa",
"searchMonster": "Buscar monstruo...",
"changeRelic": "Cambiar artefacto",
"deleteRelic": "Borrar artefacto",
"deleteRelicConfirm": "¿Seguro que quieres borrar el artefacto en este espacio?",
"setEffects": "Configurar Efectos",
"details": "Detalles",
"normal": "Ataque Básico",
"bpskill": "Habilidad",
"maze": "Técnica",
"ultra": "Habilidad Definitiva",
"servantskill": "Habilidad de Mnemoduende",
"severaltalent": "Talento de Mnemoduende",
"singleattack": "Ataque Individual",
"enhance": "Mejorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Ráfaga",
"restore": "Restaurar",
"support": "Soporte",
"aoeattack": "Ataque en Área",
"impair": "Debilitación",
"bounce": "Rebote",
"active": "Activo",
"defence": "Defensa",
"inactive": "Inactivo",
"maxAll": "Maximizar Todo",
"maxAllSuccess": "Habilidades maximizadas con éxito.",
"maxAllFailed": "Fallo al maximizar habilidades.",
"noRelicEquipped": "Sin artefacto equipado",
"anomalyArbitration": "Arbitraje Atípico",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Seleccionar evento PEAK",
"mode": "Modo",
"selectMode": "Seleccionar un modo",
"rollBack": "Retroceder",
"upRoll": "Subir Mejora",
"downRoll": "Bajar Mejora",
"actions": "Acciones",
"avatars": "Avatares",
"quickView": "Vista Rápida",
"extraSetting": "Ajustes Extra",
"disableCensorship": "Desactivar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Conteo de Ciclos",
"pleaseSelectAllSubStats": "Por favor selecciona todas las sub-estadísticas",
"subStatRollCountCannotBeZero": "El conteo de mejoras no puede ser cero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personaje Multivía",
"mainPath": "Vía Principal",
"march7Path": "Vía de Siete de Marzo",
"challenge": "Desafío",
"skipNode": "Saltar Nodo",
"disableSkip": "Desactivar salto",
"skipNode1": "Saltar nodo 1",
"skipNode2": "Saltar nodo 2",
"extraFeatures": "Características Extra",
"detailTheoryCraft": "Permite personalizar los ciclos y la vida del enemigo en los ajustes.",
"detailSkipNode": "Permite saltar el Nodo 1 o 2 en Memoria del Caos o Pura Ficción.",
"detailChallengePeak": "Permite cambiar la temporada actual de Arbitraje Atípico.",
"detailHiddenUi": "Oculta la interfaz del juego.",
"detailDisableCensorship": "Desactiva la censura dentro del juego.",
"detailMultipathCharacter": "Permite cambiar la Vía de algunos personajes.",
"trailblazer": "Trazacaminos",
"listExtraEffect": "Lista de Efectos Extra",
"extra": "Extra",
"customLineup": "Alineación de equipo personalizada"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Outils Firefly par Firefly Shelter"
},
"DataPage": {
"skillType": "Type de Compétence",
"skillName": "Nom de Compétence",
"character": "Personnage",
"id": "Id",
"path": "Voie",
"rarity": "Rareté",
"element": "Élément",
"technique": "Technique",
"talent": "Talent",
"basic": "Attaque Normale",
"skill": "Compétence",
"ultimate": "Ultime",
"servant": "Serviteur",
"damage": "Dégâts",
"type": "Type",
"warrior": "La Destruction",
"knight": "La Préservation",
"mage": "L'Érudition",
"priest": "L'Abondance",
"rogue": "La Chasse",
"shaman": "L'Harmonie",
"warlock": "La Nihilité",
"memory": "Le Souvenir",
"elation": "L'Allégresse",
"fire": "Feu",
"ice": "Glace",
"imaginary": "Imaginaire",
"physical": "Physique",
"quantum": "Quantique",
"thunder": "Foudre",
"wind": "Vent",
"hp": "PV",
"atk": "ATQ",
"speed": "VIT",
"critRate": "Taux CRIT",
"critDmg": "DGT CRIT",
"breakEffect": "Effet de Rupture",
"effectRes": "RÉS aux effets",
"energyRegenerationRate": "Taux de régénération d'énergie",
"effectHitRate": "Chances d'application d'effets",
"outgoingHealingBoost": "Augmentation des soins",
"fireDmgBoost": "Bonus de dégâts de Feu",
"iceDmgBoost": "Bonus de dégâts de Glace",
"imaginaryDmgBoost": "Bonus de dégâts Imaginaire",
"physicalDmgBoost": "Bonus de dégâts Physique",
"quantumDmgBoost": "Bonus de dégâts Quantique",
"thunderDmgBoost": "Bonus de dégâts de Foudre",
"windDmgBoost": "Bonus de dégâts de Vent",
"pursued": "Dégâts additionnels",
"true damage": "Dégâts bruts",
"elationdamage": "Dégâts d'Allégresse",
"follow-up": "Dégâts d'attaque de suivi",
"elemental damage": "Dégâts de Rupture et Super Rupture",
"dot": "Dégâts sur la durée",
"qte": "Compétence QTE",
"level": "Niveau",
"relics": "Reliques",
"eidolons": "Eidolons",
"lightcones": "Cônes de Lumière",
"loadData": "Charger les données",
"exportData": "Exporter les données",
"connectSetting": "Paramètres de connexion",
"connected": "Connecté",
"unconnected": "Déconnecté",
"psConnection": "Connexion PS",
"connectionType": "Type de connexion",
"status": "Statut",
"connectPs": "Connecter PS",
"disconnect": "Déconnecter",
"other": "Autre",
"freeSr": "FreeSR",
"database": "Base de données",
"enka": "Enka",
"monsterSetting": "Paramètre de monstres",
"serverUrl": "URL du Serveur",
"privateType": "Type privé",
"local": "Local",
"server": "Serveur",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"placeholderServerUrl": "Entrer l'URL du serveur",
"placeholderUsername": "Entrer le nom d'utilisateur",
"placeholderPassword": "Entrer le mot de passe",
"connectedSuccess": "Connecté au PS avec succès",
"connectedFailed": "Échec de connexion au PS",
"syncSuccess": "Données synchronisées au PS avec succès",
"syncFailed": "Échec de synchronisation au PS",
"sync": "Synchroniser",
"importSetting": "Paramètres d'importation",
"profile": "Profil",
"default": "Défaut",
"copyProfiles": "Copier les profils",
"addNewProfile": "Ajouter un nouveau profil",
"createNewProfile": "Créer un nouveau profil",
"editProfile": "Éditer le profil",
"placeholderProfileName": "Entrer le nom du profil",
"profileName": "Nom du profil",
"create": "Créer",
"update": "Mettre à jour",
"characterInformation": "Informations du personnage",
"skills": "Compétences",
"showcaseCard": "Carte de présentation",
"comingSoon": "Bientôt disponible",
"characterName": "Nom du personnage",
"placeholderCharacter": "Entrer le nom du personnage",
"characterSettings": "Paramètres du personnage",
"levelConfiguration": "Configuration de niveau",
"characterLevel": "Niveau du personnage",
"max": "MAX",
"ultimateEnergy": "Énergie de l'Ultime",
"currentEnergy": "Énergie actuelle",
"setTo50": "Régler sur 50%",
"battleConfiguration": "Configuration de combat",
"useTechnique": "Utiliser Technique",
"techniqueNote": "Activer les effets de technique avant le combat",
"enhancement": "Amélioration",
"enhancementLevel": "Niveau d'amélioration",
"origin": "Origine",
"enhancedNote": "Les améliorations élevées débloquent des compétences",
"lightconeEquipment": "Équipement de Cône de Lumière",
"lightconeSettings": "Paramètres de Cône de Lumière",
"placeholderLevel": "Entrer le niveau",
"superimpositionRank": "Rang de Superposition",
"ranksNote": "Les rangs élevés offrent des effets plus forts",
"changeLightcone": "Changer de Cône de Lumière",
"removeLightcone": "Retirer le Cône de Lumière",
"equipLightcone": "Équiper un Cône de Lumière",
"noLightconeEquipped": "Aucun Cône de Lumière",
"equipLightconeNote": "Équipez un cône pour renforcer votre personnage",
"filter": "Filtre",
"selectedCharacters": "Personnages sélectionnés",
"selectedProfiles": "Profils sélectionnés",
"clearAll": "Tout effacer",
"selectAll": "Tout sélectionner",
"copy": "Copier",
"copied": "Copié",
"noAvatarSelected": "Aucun personnage sélectionné",
"noAvatarToCopySelected": "Aucun personnage à copier sélectionné",
"pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil",
"pleaseEnterUid": "Veuillez entrer un UID",
"failedToFetchEnkaData": "Échec de récupération des données Enka",
"pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage",
"noDataToImport": "Aucune donnée à importer",
"pleaseSelectAFile": "Veuillez sélectionner un fichier",
"fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide",
"importEnkaDataSuccess": "Importation Enka réussie",
"importFreeSRDataSuccess": "Importation FreeSR réussie",
"importDatabaseSuccess": "Importation de base de données réussie",
"getData": "Obtenir des données",
"import": "Importer",
"freeSRImport": "Importation FreeSR",
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
"pickAFile": "Choisir un fichier",
"lightConeSetting": "Paramètre de Cône de Lumière",
"relicMaker": "Créateur de Reliques",
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
"relicSavedSuccessfully": "Relique sauvegardée avec succès",
"mainSettings": "Paramètres principaux",
"mainStat": "Statistique principale",
"set": "Set",
"pleaseSelectASet": "Veuillez sélectionner un set",
"effectBonus": "Bonus d'effet",
"totalRoll": "Total d'améliorations",
"randomizeStats": "Statistiques aléatoires",
"randomizeRolls": "Améliorations aléatoires",
"selectASubStat": "Sélectionner une stat secondaire",
"selectASet": "Sélectionner un set",
"selectAMainStat": "Sélectionner une stat principale",
"save": "Sauvegarder",
"reset": "Réinitialiser",
"roll": "Amélioration",
"step": "Étape",
"memoryOfChaos": "Mémoire du Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Ombre Apocalyptique",
"customEnemy": "Ennemi personnalisé",
"simulatedUniverse": "Univers Simulé",
"floor": "Étage",
"side": "Côté",
"wave": "Vague",
"stage": "Niveau",
"useCycleCount": "Utiliser le compteur de cycles ?",
"useTurbulenceBuff": "Utiliser le buff de turbulence ?",
"firstHalfEnemies": "Ennemis première moitié",
"secondHalfEnemies": "Ennemis deuxième moitié",
"firstNodeEnemies": "Ennemis du nœud 1",
"secondNodeEnemies": "Ennemis du nœud 2",
"thirdNodeEnemies": "Ennemis du nœud 3",
"firstNode": "Nœud 1",
"secondNode": "Nœud 2",
"thirdNode": "Nœud 3",
"listEnemies": "Liste des ennemis",
"turbulenceBuff": "Buff de Turbulence",
"noEventSelected": "Aucun événement sélectionné",
"noTurbulenceBuff": "Aucun Buff de Turbulence",
"upper": "Supérieur",
"lower": "Inférieur",
"upperToLower": "Supérieur -> Inférieur",
"lowerToUpper": "Inférieur -> Supérieur",
"selectMOCEvent": "Sélectionner un événement MOC",
"selectPFEvent": "Sélectionner un événement PF",
"selectASEvent": "Sélectionner un événement AS",
"selectCEEvent": "Sélectionner un événement CE",
"selectEvent": "Sélectionner un événement",
"selectFloor": "Sélectionner un étage",
"selectSide": "Sélectionner un côté",
"selectBuff": "Sélectionner un buff",
"selectStage": "Sélectionner un niveau",
"previous": "Précédent",
"next": "Suivant",
"noMonstersFound": "Aucun monstre trouvé",
"addNewWave": "Ajouter une nouvelle vague",
"searchStage": "Chercher un niveau...",
"noStageFound": "Aucun niveau trouvé",
"searchMonster": "Chercher un monstre...",
"changeRelic": "Changer de relique",
"deleteRelic": "Supprimer la relique",
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
"setEffects": "Définir les effets",
"details": "Détails",
"normal": "Attaque Normale",
"bpskill": "Compétence",
"maze": "Technique",
"ultra": "Ultime",
"servantskill": "Compétence de Mémolutin",
"severaltalent": "Talent de Mémolutin",
"singleattack": "Attaque à cible unique",
"enhance": "Renforcement",
"summon": "Invocation",
"mazeattack": "Attaque de Technique",
"blast": "Diffusion",
"restore": "Restauration",
"support": "Soutien",
"aoeattack": "Attaque de zone",
"impair": "Malus",
"bounce": "Rebond",
"active": "Actif",
"defence": "Défense",
"inactive": "Inactif",
"maxAll": "Tout maximiser",
"maxAllSuccess": "Compétences maximisées avec succès.",
"maxAllFailed": "Échec de maximisation.",
"noRelicEquipped": "Aucune relique équipée",
"anomalyArbitration": "Arbitrage d'Anomalie",
"normalMode": "Mode Normal",
"hardMode": "Mode Difficile",
"selectPEAKEvent": "Sélectionner un événement PEAK",
"mode": "Mode",
"selectMode": "Sélectionner un mode",
"rollBack": "Annuler l'étape",
"upRoll": "Augmenter l'amélioration",
"downRoll": "Diminuer l'amélioration",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Aperçu rapide",
"extraSetting": "Paramètres supplémentaires",
"disableCensorship": "Désactiver la censure",
"hideUI": "Cacher l'UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Nombre de cycles",
"pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires",
"subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personnage Multi-Voies",
"mainPath": "Voie Principale",
"march7Path": "Voie de March 7th",
"challenge": "Défi",
"skipNode": "Passer le nœud",
"disableSkip": "Désactiver le passage",
"skipNode1": "Passer le nœud 1",
"skipNode2": "Passer le nœud 2",
"extraFeatures": "Fonctionnalités supplémentaires",
"detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.",
"detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.",
"detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.",
"detailHiddenUi": "Cachera l'interface du jeu.",
"detailDisableCensorship": "Désactivera la censure du jeu.",
"detailMultipathCharacter": "Permet de changer la Voie de certains personnages.",
"trailblazer": "Pionnier",
"listExtraEffect": "Liste des effets supplémentaires",
"extra": "Extra",
"customLineup": "Composition personnalisée"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Outils Firefly par Firefly Shelter"
},
"DataPage": {
"skillType": "Type de Compétence",
"skillName": "Nom de Compétence",
"character": "Personnage",
"id": "Id",
"path": "Voie",
"rarity": "Rareté",
"element": "Élément",
"technique": "Technique",
"talent": "Talent",
"basic": "Attaque Normale",
"skill": "Compétence",
"ultimate": "Ultime",
"servant": "Serviteur",
"damage": "Dégâts",
"type": "Type",
"warrior": "La Destruction",
"knight": "La Préservation",
"mage": "L'Érudition",
"priest": "L'Abondance",
"rogue": "La Chasse",
"shaman": "L'Harmonie",
"warlock": "La Nihilité",
"memory": "Le Souvenir",
"elation": "L'Allégresse",
"fire": "Feu",
"ice": "Glace",
"imaginary": "Imaginaire",
"physical": "Physique",
"quantum": "Quantique",
"thunder": "Foudre",
"wind": "Vent",
"hp": "PV",
"atk": "ATQ",
"speed": "VIT",
"critRate": "Taux CRIT",
"critDmg": "DGT CRIT",
"breakEffect": "Effet de Rupture",
"effectRes": "RÉS aux effets",
"energyRegenerationRate": "Taux de régénération d'énergie",
"effectHitRate": "Chances d'application d'effets",
"outgoingHealingBoost": "Augmentation des soins",
"fireDmgBoost": "Bonus de dégâts de Feu",
"iceDmgBoost": "Bonus de dégâts de Glace",
"imaginaryDmgBoost": "Bonus de dégâts Imaginaire",
"physicalDmgBoost": "Bonus de dégâts Physique",
"quantumDmgBoost": "Bonus de dégâts Quantique",
"thunderDmgBoost": "Bonus de dégâts de Foudre",
"windDmgBoost": "Bonus de dégâts de Vent",
"pursued": "Dégâts additionnels",
"true damage": "Dégâts bruts",
"elationdamage": "Dégâts d'Allégresse",
"follow-up": "Dégâts d'attaque de suivi",
"elemental damage": "Dégâts de Rupture et Super Rupture",
"dot": "Dégâts sur la durée",
"qte": "Compétence QTE",
"level": "Niveau",
"relics": "Reliques",
"eidolons": "Eidolons",
"lightcones": "Cônes de Lumière",
"loadData": "Charger les données",
"exportData": "Exporter les données",
"connectSetting": "Paramètres de connexion",
"connected": "Connecté",
"unconnected": "Déconnecté",
"psConnection": "Connexion PS",
"connectionType": "Type de connexion",
"status": "Statut",
"connectPs": "Connecter PS",
"disconnect": "Déconnecter",
"other": "Autre",
"freeSr": "FreeSR",
"database": "Base de données",
"enka": "Enka",
"monsterSetting": "Paramètre de monstres",
"serverUrl": "URL du Serveur",
"privateType": "Type privé",
"local": "Local",
"server": "Serveur",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"placeholderServerUrl": "Entrer l'URL du serveur",
"placeholderUsername": "Entrer le nom d'utilisateur",
"placeholderPassword": "Entrer le mot de passe",
"connectedSuccess": "Connecté au PS avec succès",
"connectedFailed": "Échec de connexion au PS",
"syncSuccess": "Données synchronisées au PS avec succès",
"syncFailed": "Échec de synchronisation au PS",
"sync": "Synchroniser",
"importSetting": "Paramètres d'importation",
"profile": "Profil",
"default": "Défaut",
"copyProfiles": "Copier les profils",
"addNewProfile": "Ajouter un nouveau profil",
"createNewProfile": "Créer un nouveau profil",
"editProfile": "Éditer le profil",
"placeholderProfileName": "Entrer le nom du profil",
"profileName": "Nom du profil",
"create": "Créer",
"update": "Mettre à jour",
"characterInformation": "Informations du personnage",
"skills": "Compétences",
"showcaseCard": "Carte de présentation",
"comingSoon": "Bientôt disponible",
"characterName": "Nom du personnage",
"placeholderCharacter": "Entrer le nom du personnage",
"characterSettings": "Paramètres du personnage",
"levelConfiguration": "Configuration de niveau",
"characterLevel": "Niveau du personnage",
"max": "MAX",
"ultimateEnergy": "Énergie de l'Ultime",
"currentEnergy": "Énergie actuelle",
"setTo50": "Régler sur 50%",
"battleConfiguration": "Configuration de combat",
"useTechnique": "Utiliser Technique",
"techniqueNote": "Activer les effets de technique avant le combat",
"enhancement": "Amélioration",
"enhancementLevel": "Niveau d'amélioration",
"origin": "Origine",
"enhancedNote": "Les améliorations élevées débloquent des compétences",
"lightconeEquipment": "Équipement de Cône de Lumière",
"lightconeSettings": "Paramètres de Cône de Lumière",
"placeholderLevel": "Entrer le niveau",
"superimpositionRank": "Rang de Superposition",
"ranksNote": "Les rangs élevés offrent des effets plus forts",
"changeLightcone": "Changer de Cône de Lumière",
"removeLightcone": "Retirer le Cône de Lumière",
"equipLightcone": "Équiper un Cône de Lumière",
"noLightconeEquipped": "Aucun Cône de Lumière",
"equipLightconeNote": "Équipez un cône pour renforcer votre personnage",
"filter": "Filtre",
"selectedCharacters": "Personnages sélectionnés",
"selectedProfiles": "Profils sélectionnés",
"clearAll": "Tout effacer",
"selectAll": "Tout sélectionner",
"copy": "Copier",
"copied": "Copié",
"noAvatarSelected": "Aucun personnage sélectionné",
"noAvatarToCopySelected": "Aucun personnage à copier sélectionné",
"pleaseSelectAtLeastOneProfile": "Veuillez sélectionner au moins un profil",
"pleaseEnterUid": "Veuillez entrer un UID",
"failedToFetchEnkaData": "Échec de récupération des données Enka",
"pleaseSelectAtLeastOneCharacter": "Veuillez sélectionner au moins un personnage",
"noDataToImport": "Aucune donnée à importer",
"pleaseSelectAFile": "Veuillez sélectionner un fichier",
"fileMustBeAValidJsonFile": "Le fichier doit être un JSON valide",
"importEnkaDataSuccess": "Importation Enka réussie",
"importFreeSRDataSuccess": "Importation FreeSR réussie",
"importDatabaseSuccess": "Importation de base de données réussie",
"getData": "Obtenir des données",
"import": "Importer",
"freeSRImport": "Importation FreeSR",
"onlySupportFreeSRJsonFile": "Supporte uniquement les fichiers JSON FreeSR",
"pickAFile": "Choisir un fichier",
"lightConeSetting": "Paramètre de Cône de Lumière",
"relicMaker": "Créateur de Reliques",
"pleaseSelectAllOptions": "Veuillez sélectionner toutes les options",
"relicSavedSuccessfully": "Relique sauvegardée avec succès",
"mainSettings": "Paramètres principaux",
"mainStat": "Statistique principale",
"set": "Set",
"pleaseSelectASet": "Veuillez sélectionner un set",
"effectBonus": "Bonus d'effet",
"totalRoll": "Total d'améliorations",
"randomizeStats": "Statistiques aléatoires",
"randomizeRolls": "Améliorations aléatoires",
"selectASubStat": "Sélectionner une stat secondaire",
"selectASet": "Sélectionner un set",
"selectAMainStat": "Sélectionner une stat principale",
"save": "Sauvegarder",
"reset": "Réinitialiser",
"roll": "Amélioration",
"step": "Étape",
"memoryOfChaos": "Mémoire du Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Ombre Apocalyptique",
"customEnemy": "Ennemi personnalisé",
"simulatedUniverse": "Univers Simulé",
"floor": "Étage",
"side": "Côté",
"wave": "Vague",
"stage": "Niveau",
"useCycleCount": "Utiliser le compteur de cycles ?",
"useTurbulenceBuff": "Utiliser le buff de turbulence ?",
"firstHalfEnemies": "Ennemis première moitié",
"secondHalfEnemies": "Ennemis deuxième moitié",
"firstNodeEnemies": "Ennemis du nœud 1",
"secondNodeEnemies": "Ennemis du nœud 2",
"thirdNodeEnemies": "Ennemis du nœud 3",
"firstNode": "Nœud 1",
"secondNode": "Nœud 2",
"thirdNode": "Nœud 3",
"listEnemies": "Liste des ennemis",
"turbulenceBuff": "Buff de Turbulence",
"noEventSelected": "Aucun événement sélectionné",
"noTurbulenceBuff": "Aucun Buff de Turbulence",
"upper": "Supérieur",
"lower": "Inférieur",
"upperToLower": "Supérieur -> Inférieur",
"lowerToUpper": "Inférieur -> Supérieur",
"selectMOCEvent": "Sélectionner un événement MOC",
"selectPFEvent": "Sélectionner un événement PF",
"selectASEvent": "Sélectionner un événement AS",
"selectCEEvent": "Sélectionner un événement CE",
"selectEvent": "Sélectionner un événement",
"selectFloor": "Sélectionner un étage",
"selectSide": "Sélectionner un côté",
"selectBuff": "Sélectionner un buff",
"selectStage": "Sélectionner un niveau",
"previous": "Précédent",
"next": "Suivant",
"noMonstersFound": "Aucun monstre trouvé",
"addNewWave": "Ajouter une nouvelle vague",
"searchStage": "Chercher un niveau...",
"noStageFound": "Aucun niveau trouvé",
"searchMonster": "Chercher un monstre...",
"changeRelic": "Changer de relique",
"deleteRelic": "Supprimer la relique",
"deleteRelicConfirm": "Êtes-vous sûr de vouloir supprimer la relique de cet emplacement ?",
"setEffects": "Définir les effets",
"details": "Détails",
"normal": "Attaque Normale",
"bpskill": "Compétence",
"maze": "Technique",
"ultra": "Ultime",
"servantskill": "Compétence de Mémolutin",
"severaltalent": "Talent de Mémolutin",
"singleattack": "Attaque à cible unique",
"enhance": "Renforcement",
"summon": "Invocation",
"mazeattack": "Attaque de Technique",
"blast": "Diffusion",
"restore": "Restauration",
"support": "Soutien",
"aoeattack": "Attaque de zone",
"impair": "Malus",
"bounce": "Rebond",
"active": "Actif",
"defence": "Défense",
"inactive": "Inactif",
"maxAll": "Tout maximiser",
"maxAllSuccess": "Compétences maximisées avec succès.",
"maxAllFailed": "Échec de maximisation.",
"noRelicEquipped": "Aucune relique équipée",
"anomalyArbitration": "Arbitrage d'Anomalie",
"normalMode": "Mode Normal",
"hardMode": "Mode Difficile",
"selectPEAKEvent": "Sélectionner un événement PEAK",
"mode": "Mode",
"selectMode": "Sélectionner un mode",
"rollBack": "Annuler l'étape",
"upRoll": "Augmenter l'amélioration",
"downRoll": "Diminuer l'amélioration",
"actions": "Actions",
"avatars": "Avatars",
"quickView": "Aperçu rapide",
"extraSetting": "Paramètres supplémentaires",
"disableCensorship": "Désactiver la censure",
"hideUI": "Cacher l'UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Nombre de cycles",
"pleaseSelectAllSubStats": "Veuillez sélectionner toutes les stats secondaires",
"subStatRollCountCannotBeZero": "Les améliorations de stat secondaire ne peuvent être à zéro",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personnage Multi-Voies",
"mainPath": "Voie Principale",
"march7Path": "Voie de March 7th",
"challenge": "Défi",
"skipNode": "Passer le nœud",
"disableSkip": "Désactiver le passage",
"skipNode1": "Passer le nœud 1",
"skipNode2": "Passer le nœud 2",
"extraFeatures": "Fonctionnalités supplémentaires",
"detailTheoryCraft": "Permet de personnaliser le nombre de cycles et d'ajuster les PV ennemis.",
"detailSkipNode": "Permet de passer le nœud (1 ou 2) dans Mémoire du Chaos ou Pure Fiction.",
"detailChallengePeak": "Permet de changer la saison de Peak dans l'anomalie actuelle.",
"detailHiddenUi": "Cachera l'interface du jeu.",
"detailDisableCensorship": "Désactivera la censure du jeu.",
"detailMultipathCharacter": "Permet de changer la Voie de certains personnages.",
"trailblazer": "Pionnier",
"listExtraEffect": "Liste des effets supplémentaires",
"extra": "Extra",
"customLineup": "Composition personnalisée"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Alat Firefly oleh Firefly Shelter"
},
"DataPage": {
"skillType": "Tipe Skill",
"skillName": "Nama Skill",
"character": "Karakter",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Elemen",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Tipe",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Lightning",
"wind": "Wind",
"hp": "HP",
"atk": "ATK",
"speed": "SPD",
"critRate": "CRIT Rate",
"critDmg": "CRIT DMG",
"breakEffect": "Break Effect",
"effectRes": "Effect RES",
"energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire DMG Boost",
"iceDmgBoost": "Ice DMG Boost",
"imaginaryDmgBoost": "Imaginary DMG Boost",
"physicalDmgBoost": "Physical DMG Boost",
"quantumDmgBoost": "Quantum DMG Boost",
"thunderDmgBoost": "Lightning DMG Boost",
"windDmgBoost": "Wind DMG Boost",
"pursued": "Additional Damage",
"true damage": "True Damage",
"elationdamage": "Elation Damage",
"follow-up": "Follow-up Attack Damage",
"elemental damage": "Break dan Super Break Damage",
"dot": "Damage Over Time",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relic",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "Muat data",
"exportData": "Ekspor data",
"connectSetting": "Pengaturan Koneksi",
"connected": "Terhubung",
"unconnected": "Tidak terhubung",
"psConnection": "Koneksi PS",
"connectionType": "Tipe Koneksi",
"status": "Status",
"connectPs": "Hubungkan PS",
"disconnect": "Putuskan sambungan",
"other": "Lainnya",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Pengaturan Monster",
"serverUrl": "URL Server",
"privateType": "Tipe Private",
"local": "Lokal",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Masukkan URL server",
"placeholderUsername": "Masukkan username",
"placeholderPassword": "Masukkan password",
"connectedSuccess": "Berhasil terhubung ke PS",
"connectedFailed": "Gagal terhubung ke PS",
"syncSuccess": "Berhasil sinkronisasi data ke PS",
"syncFailed": "Gagal sinkronisasi data ke PS",
"sync": "Sinkronisasi",
"importSetting": "Pengaturan Impor",
"profile": "Profil",
"default": "Default",
"copyProfiles": "Salin profil",
"addNewProfile": "Tambah profil baru",
"createNewProfile": "Buat profil baru",
"editProfile": "Edit profil",
"placeholderProfileName": "Masukkan nama profil",
"profileName": "Nama profil",
"create": "Buat",
"update": "Perbarui",
"characterInformation": "Informasi Karakter",
"skills": "Skill",
"showcaseCard": "Showcase Card",
"comingSoon": "Segera hadir",
"characterName": "Nama Karakter",
"placeholderCharacter": "Masukkan nama karakter",
"characterSettings": "Pengaturan Karakter",
"levelConfiguration": "Konfigurasi Level",
"characterLevel": "Level Karakter",
"max": "MAX",
"ultimateEnergy": "Energy Ultimate",
"currentEnergy": "Energy Saat Ini",
"setTo50": "Atur ke 50%",
"battleConfiguration": "Konfigurasi Pertempuran",
"useTechnique": "Gunakan Technique",
"techniqueNote": "Aktifkan efek technique sebelum pertempuran",
"enhancement": "Enhancement",
"enhancementLevel": "Level Enhancement",
"origin": "Asli",
"enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan",
"lightconeEquipment": "Peralatan Light Cone",
"lightconeSettings": "Pengaturan Light Cone",
"placeholderLevel": "Masukkan level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Rank lebih tinggi memberikan efek lebih kuat",
"changeLightcone": "Ganti Light Cone",
"removeLightcone": "Lepas Light Cone",
"equipLightcone": "Pakai Light Cone",
"noLightconeEquipped": "Tidak Ada Light Cone",
"equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu",
"filter": "Filter",
"selectedCharacters": "Karakter Dipilih",
"selectedProfiles": "Profil Dipilih",
"clearAll": "Bersihkan Semua",
"selectAll": "Pilih Semua",
"copy": "Salin",
"copied": "Tersalin",
"noAvatarSelected": "Tidak ada karakter dipilih",
"noAvatarToCopySelected": "Tidak ada karakter untuk disalin",
"pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil",
"pleaseEnterUid": "Silakan masukkan UID",
"failedToFetchEnkaData": "Gagal mengambil data Enka",
"pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter",
"noDataToImport": "Tidak ada data untuk diimpor",
"pleaseSelectAFile": "Silakan pilih file",
"fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid",
"importEnkaDataSuccess": "Impor data Enka berhasil",
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
"importDatabaseSuccess": "Impor database berhasil",
"getData": "Dapatkan Data",
"import": "Impor",
"freeSRImport": "Impor FreeSR",
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
"pickAFile": "Pilih sebuah file",
"lightConeSetting": "Pengaturan Light Cone",
"relicMaker": "Pembuat Relic",
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
"relicSavedSuccessfully": "Relic berhasil disimpan",
"mainSettings": "Pengaturan Utama",
"mainStat": "Statistik Utama",
"set": "Set",
"pleaseSelectASet": "Pilih sebuah set",
"effectBonus": "Bonus Efek",
"totalRoll": "Total Roll",
"randomizeStats": "Acak Statistik",
"randomizeRolls": "Acak Roll",
"selectASubStat": "Pilih sub stat",
"selectASet": "Pilih sebuah set",
"selectAMainStat": "Pilih stat utama",
"save": "Simpan",
"reset": "Reset",
"roll": "Roll",
"step": "Langkah",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Musuh Kustom",
"simulatedUniverse": "Simulated Universe",
"floor": "Lantai",
"side": "Sisi",
"wave": "Gelombang",
"stage": "Tahap",
"useCycleCount": "Gunakan hitungan siklus?",
"useTurbulenceBuff": "Gunakan buff turbulence?",
"firstHalfEnemies": "Musuh paruh pertama",
"secondHalfEnemies": "Musuh paruh kedua",
"firstNodeEnemies": "Musuh Node 1",
"secondNodeEnemies": "Musuh Node 2",
"thirdNodeEnemies": "Musuh Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"listEnemies": "Daftar musuh",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "Tidak ada event dipilih",
"noTurbulenceBuff": "Tanpa Turbulence Buff",
"upper": "Atas",
"lower": "Bawah",
"upperToLower": "Atas -> Bawah",
"lowerToUpper": "Bawah -> Atas",
"selectMOCEvent": "Pilih Event MOC",
"selectPFEvent": "Pilih Event PF",
"selectASEvent": "Pilih Event AS",
"selectCEEvent": "Pilih Event CE",
"selectEvent": "Pilih Event",
"selectFloor": "Pilih Lantai",
"selectSide": "Pilih Sisi",
"selectBuff": "Pilih Buff",
"selectStage": "Pilih Tahap",
"previous": "Sebelumnya",
"next": "Selanjutnya",
"noMonstersFound": "Tidak ada monster ditemukan",
"addNewWave": "Tambah Gelombang Baru",
"searchStage": "Cari tahap...",
"noStageFound": "Tahap tidak ditemukan",
"searchMonster": "Cari monster...",
"changeRelic": "Ganti relic",
"deleteRelic": "Hapus relic",
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
"setEffects": "Atur Efek",
"details": "Detail",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"servantskill": "Skill Memosprite",
"severaltalent": "Talent Memosprite",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Aktif",
"defence": "Defence",
"inactive": "Nonaktif",
"maxAll": "Maksimalkan Semua",
"maxAllSuccess": "Berhasil memaksimalkan level skill.",
"maxAllFailed": "Gagal memaksimalkan level skill.",
"noRelicEquipped": "Tidak ada relic dipakai",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Mode Normal",
"hardMode": "Mode Sulit",
"selectPEAKEvent": "Pilih Event PEAK",
"mode": "Mode",
"selectMode": "Pilih Mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Aksi",
"avatars": "Avatar",
"quickView": "Tampilan Cepat",
"extraSetting": "Pengaturan Ekstra",
"disableCensorship": "Nonaktifkan Sensor",
"hideUI": "Sembunyikan UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Hitungan Siklus",
"pleaseSelectAllSubStats": "Pilih semua sub statistik",
"subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Karakter Multipath",
"mainPath": "Path Utama",
"march7Path": "Path March 7th",
"challenge": "Tantangan",
"skipNode": "Lewati Node",
"disableSkip": "Nonaktifkan lewati",
"skipNode1": "Lewati node 1",
"skipNode2": "Lewati node 2",
"extraFeatures": "Fitur Ekstra",
"detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.",
"detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.",
"detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.",
"detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.",
"detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.",
"detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Daftar Efek Ekstra",
"extra": "Ekstra",
"customLineup": "Lineup Kustom"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Alat Firefly oleh Firefly Shelter"
},
"DataPage": {
"skillType": "Tipe Skill",
"skillName": "Nama Skill",
"character": "Karakter",
"id": "Id",
"path": "Path",
"rarity": "Rarity",
"element": "Elemen",
"technique": "Technique",
"talent": "Talent",
"basic": "Basic Attack",
"skill": "Skill",
"ultimate": "Ultimate",
"servant": "Servant",
"damage": "Damage",
"type": "Tipe",
"warrior": "The Destruction",
"knight": "The Preservation",
"mage": "The Erudition",
"priest": "The Abundance",
"rogue": "The Hunt",
"shaman": "The Harmony",
"warlock": "The Nihility",
"memory": "The Remembrance",
"elation": "The Elation",
"fire": "Fire",
"ice": "Ice",
"imaginary": "Imaginary",
"physical": "Physical",
"quantum": "Quantum",
"thunder": "Lightning",
"wind": "Wind",
"hp": "HP",
"atk": "ATK",
"speed": "SPD",
"critRate": "CRIT Rate",
"critDmg": "CRIT DMG",
"breakEffect": "Break Effect",
"effectRes": "Effect RES",
"energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire DMG Boost",
"iceDmgBoost": "Ice DMG Boost",
"imaginaryDmgBoost": "Imaginary DMG Boost",
"physicalDmgBoost": "Physical DMG Boost",
"quantumDmgBoost": "Quantum DMG Boost",
"thunderDmgBoost": "Lightning DMG Boost",
"windDmgBoost": "Wind DMG Boost",
"pursued": "Additional Damage",
"true damage": "True Damage",
"elationdamage": "Elation Damage",
"follow-up": "Follow-up Attack Damage",
"elemental damage": "Break dan Super Break Damage",
"dot": "Damage Over Time",
"qte": "QTE Skill",
"level": "Level",
"relics": "Relic",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "Muat data",
"exportData": "Ekspor data",
"connectSetting": "Pengaturan Koneksi",
"connected": "Terhubung",
"unconnected": "Tidak terhubung",
"psConnection": "Koneksi PS",
"connectionType": "Tipe Koneksi",
"status": "Status",
"connectPs": "Hubungkan PS",
"disconnect": "Putuskan sambungan",
"other": "Lainnya",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Pengaturan Monster",
"serverUrl": "URL Server",
"privateType": "Tipe Private",
"local": "Lokal",
"server": "Server",
"username": "Username",
"password": "Password",
"placeholderServerUrl": "Masukkan URL server",
"placeholderUsername": "Masukkan username",
"placeholderPassword": "Masukkan password",
"connectedSuccess": "Berhasil terhubung ke PS",
"connectedFailed": "Gagal terhubung ke PS",
"syncSuccess": "Berhasil sinkronisasi data ke PS",
"syncFailed": "Gagal sinkronisasi data ke PS",
"sync": "Sinkronisasi",
"importSetting": "Pengaturan Impor",
"profile": "Profil",
"default": "Default",
"copyProfiles": "Salin profil",
"addNewProfile": "Tambah profil baru",
"createNewProfile": "Buat profil baru",
"editProfile": "Edit profil",
"placeholderProfileName": "Masukkan nama profil",
"profileName": "Nama profil",
"create": "Buat",
"update": "Perbarui",
"characterInformation": "Informasi Karakter",
"skills": "Skill",
"showcaseCard": "Showcase Card",
"comingSoon": "Segera hadir",
"characterName": "Nama Karakter",
"placeholderCharacter": "Masukkan nama karakter",
"characterSettings": "Pengaturan Karakter",
"levelConfiguration": "Konfigurasi Level",
"characterLevel": "Level Karakter",
"max": "MAX",
"ultimateEnergy": "Energy Ultimate",
"currentEnergy": "Energy Saat Ini",
"setTo50": "Atur ke 50%",
"battleConfiguration": "Konfigurasi Pertempuran",
"useTechnique": "Gunakan Technique",
"techniqueNote": "Aktifkan efek technique sebelum pertempuran",
"enhancement": "Enhancement",
"enhancementLevel": "Level Enhancement",
"origin": "Asli",
"enhancedNote": "Enhancement lebih tinggi membuka kemampuan tambahan",
"lightconeEquipment": "Peralatan Light Cone",
"lightconeSettings": "Pengaturan Light Cone",
"placeholderLevel": "Masukkan level",
"superimpositionRank": "Superimposition Rank",
"ranksNote": "Rank lebih tinggi memberikan efek lebih kuat",
"changeLightcone": "Ganti Light Cone",
"removeLightcone": "Lepas Light Cone",
"equipLightcone": "Pakai Light Cone",
"noLightconeEquipped": "Tidak Ada Light Cone",
"equipLightconeNote": "Pakai light cone untuk memperkuat karaktermu",
"filter": "Filter",
"selectedCharacters": "Karakter Dipilih",
"selectedProfiles": "Profil Dipilih",
"clearAll": "Bersihkan Semua",
"selectAll": "Pilih Semua",
"copy": "Salin",
"copied": "Tersalin",
"noAvatarSelected": "Tidak ada karakter dipilih",
"noAvatarToCopySelected": "Tidak ada karakter untuk disalin",
"pleaseSelectAtLeastOneProfile": "Pilih setidaknya satu profil",
"pleaseEnterUid": "Silakan masukkan UID",
"failedToFetchEnkaData": "Gagal mengambil data Enka",
"pleaseSelectAtLeastOneCharacter": "Pilih setidaknya satu karakter",
"noDataToImport": "Tidak ada data untuk diimpor",
"pleaseSelectAFile": "Silakan pilih file",
"fileMustBeAValidJsonFile": "File harus berupa file JSON yang valid",
"importEnkaDataSuccess": "Impor data Enka berhasil",
"importFreeSRDataSuccess": "Impor data FreeSR berhasil",
"importDatabaseSuccess": "Impor database berhasil",
"getData": "Dapatkan Data",
"import": "Impor",
"freeSRImport": "Impor FreeSR",
"onlySupportFreeSRJsonFile": "Hanya mendukung file JSON FreeSR",
"pickAFile": "Pilih sebuah file",
"lightConeSetting": "Pengaturan Light Cone",
"relicMaker": "Pembuat Relic",
"pleaseSelectAllOptions": "Silakan pilih semua opsi",
"relicSavedSuccessfully": "Relic berhasil disimpan",
"mainSettings": "Pengaturan Utama",
"mainStat": "Statistik Utama",
"set": "Set",
"pleaseSelectASet": "Pilih sebuah set",
"effectBonus": "Bonus Efek",
"totalRoll": "Total Roll",
"randomizeStats": "Acak Statistik",
"randomizeRolls": "Acak Roll",
"selectASubStat": "Pilih sub stat",
"selectASet": "Pilih sebuah set",
"selectAMainStat": "Pilih stat utama",
"save": "Simpan",
"reset": "Reset",
"roll": "Roll",
"step": "Langkah",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Musuh Kustom",
"simulatedUniverse": "Simulated Universe",
"floor": "Lantai",
"side": "Sisi",
"wave": "Gelombang",
"stage": "Tahap",
"useCycleCount": "Gunakan hitungan siklus?",
"useTurbulenceBuff": "Gunakan buff turbulence?",
"firstHalfEnemies": "Musuh paruh pertama",
"secondHalfEnemies": "Musuh paruh kedua",
"firstNodeEnemies": "Musuh Node 1",
"secondNodeEnemies": "Musuh Node 2",
"thirdNodeEnemies": "Musuh Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"listEnemies": "Daftar musuh",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "Tidak ada event dipilih",
"noTurbulenceBuff": "Tanpa Turbulence Buff",
"upper": "Atas",
"lower": "Bawah",
"upperToLower": "Atas -> Bawah",
"lowerToUpper": "Bawah -> Atas",
"selectMOCEvent": "Pilih Event MOC",
"selectPFEvent": "Pilih Event PF",
"selectASEvent": "Pilih Event AS",
"selectCEEvent": "Pilih Event CE",
"selectEvent": "Pilih Event",
"selectFloor": "Pilih Lantai",
"selectSide": "Pilih Sisi",
"selectBuff": "Pilih Buff",
"selectStage": "Pilih Tahap",
"previous": "Sebelumnya",
"next": "Selanjutnya",
"noMonstersFound": "Tidak ada monster ditemukan",
"addNewWave": "Tambah Gelombang Baru",
"searchStage": "Cari tahap...",
"noStageFound": "Tahap tidak ditemukan",
"searchMonster": "Cari monster...",
"changeRelic": "Ganti relic",
"deleteRelic": "Hapus relic",
"deleteRelicConfirm": "Yakin ingin menghapus relic di slot ini?",
"setEffects": "Atur Efek",
"details": "Detail",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"servantskill": "Skill Memosprite",
"severaltalent": "Talent Memosprite",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Aktif",
"defence": "Defence",
"inactive": "Nonaktif",
"maxAll": "Maksimalkan Semua",
"maxAllSuccess": "Berhasil memaksimalkan level skill.",
"maxAllFailed": "Gagal memaksimalkan level skill.",
"noRelicEquipped": "Tidak ada relic dipakai",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "Mode Normal",
"hardMode": "Mode Sulit",
"selectPEAKEvent": "Pilih Event PEAK",
"mode": "Mode",
"selectMode": "Pilih Mode",
"rollBack": "Roll Back",
"upRoll": "Up Roll",
"downRoll": "Down Roll",
"actions": "Aksi",
"avatars": "Avatar",
"quickView": "Tampilan Cepat",
"extraSetting": "Pengaturan Ekstra",
"disableCensorship": "Nonaktifkan Sensor",
"hideUI": "Sembunyikan UI",
"theoryCraftMode": "Mode Theorycraft",
"cycleCount": "Hitungan Siklus",
"pleaseSelectAllSubStats": "Pilih semua sub statistik",
"subStatRollCountCannotBeZero": "Hitungan roll sub stat tidak boleh nol",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Karakter Multipath",
"mainPath": "Path Utama",
"march7Path": "Path March 7th",
"challenge": "Tantangan",
"skipNode": "Lewati Node",
"disableSkip": "Nonaktifkan lewati",
"skipNode1": "Lewati node 1",
"skipNode2": "Lewati node 2",
"extraFeatures": "Fitur Ekstra",
"detailTheoryCraft": "Mengaktifkan fitur ini memungkinkan Anda menyesuaikan jumlah siklus dan menyesuaikan HP musuh.",
"detailSkipNode": "Memungkinkan Anda melewati (Node 1/Node 2) di Memory of Chaos atau Pure Fiction.",
"detailChallengePeak": "Memungkinkan perubahan musim Peak dalam Anomaly saat ini.",
"detailHiddenUi": "Mengaktifkan fitur ini akan menyembunyikan UI game.",
"detailDisableCensorship": "Mengaktifkan fitur ini akan mematikan sensor dalam game.",
"detailMultipathCharacter": "Memungkinkan perubahan Path untuk karakter tertentu.",
"trailblazer": "Trailblazer",
"listExtraEffect": "Daftar Efek Ekstra",
"extra": "Ekstra",
"customLineup": "Lineup Kustom"
}
}
+288 -288
View File
@@ -1,289 +1,289 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "スキルタイプ",
"skillName": "スキル名",
"character": "キャラクター",
"id": "ID",
"path": "運命",
"rarity": "レアリティ",
"element": "属性",
"technique": "秘技",
"talent": "天賦",
"basic": "通常攻撃",
"skill": "スキル",
"ultimate": "アルティメット",
"servant": "サーバント",
"damage": "ダメージ",
"type": "タイプ",
"warrior": "壊滅",
"knight": "存護",
"mage": "知恵",
"priest": "豊穣",
"rogue": "巡狩",
"shaman": "調和",
"warlock": "虚無",
"memory": "記憶",
"elation": "愉悦",
"fire": "火",
"ice": "氷",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
"wind": "風",
"hp": "HP",
"atk": "攻撃",
"speed": "速度",
"critRate": "会心率",
"critDmg": "会心ダメージ",
"breakEffect": "撃破特効",
"effectRes": "効果抵抗",
"energyRegenerationRate": "EP回復効率",
"effectHitRate": "効果命中率",
"outgoingHealingBoost": "回復増加",
"fireDmgBoost": "火属性ダメージ強化",
"iceDmgBoost": "氷属性ダメージ強化",
"imaginaryDmgBoost": "幻影ダメージ強化",
"physicalDmgBoost": "物理ダメージ強化",
"quantumDmgBoost": "量子ダメージ強化",
"thunderDmgBoost": "雷属性ダメージ強化",
"windDmgBoost": "風属性ダメージ強化",
"pursued": "追加ダメージ",
"true damage": "確定ダメージ",
"elationdamage": "愉悦ダメージ",
"follow-up": "追撃ダメージ",
"elemental damage": "属性ダメージ",
"dot": "継続ダメージ",
"qte": "QTEスキル",
"level": "レベル",
"relics": "遺物",
"eidolons": "エイドロン",
"lightcones": "光円錐",
"loadData": "データ読み込み",
"exportData": "データ出力",
"connectSetting": "接続設定",
"connected": "接続済み",
"unconnected": "未接続",
"psConnection": "PS接続",
"connectionType": "接続タイプ",
"status": "ステータス",
"connectPs": "PS接続",
"disconnect": "切断",
"other": "その他",
"freeSr": "FreeSR",
"database": "データベース",
"enka": "Enka",
"monsterSetting": "モンスター設定",
"serverUrl": "サーバー URL",
"privateType": "プライベートタイプ",
"local": "ローカル",
"server": "サーバー",
"username": "ユーザー名",
"password": "パスワード",
"placeholderServerUrl": "サーバー URL を入力",
"placeholderUsername": "ユーザー名を入力",
"placeholderPassword": "パスワードを入力",
"connectedSuccess": "PS接続成功",
"connectedFailed": "PS接続失敗",
"syncSuccess": "PSへのデータ同期に成功",
"syncFailed": "PSへのデータ同期に失敗",
"sync": "同期",
"importSetting": "インポート設定",
"profile": "プロファイル",
"default": "デフォルト",
"copyProfiles": "プロファイルをコピー",
"addNewProfile": "プロファイルを追加",
"createNewProfile": "新しいプロファイルを作成",
"editProfile": "プロファイル編集",
"placeholderProfileName": "プロファイル名を入力",
"profileName": "プロファイル名",
"create": "作成",
"update": "更新",
"characterInformation": "キャラクター情報",
"skills": "スキル",
"showcaseCard": "ショーケースカード",
"comingSoon": "近日公開",
"characterName": "キャラクター名",
"placeholderCharacter": "キャラクター名を入力",
"characterSettings": "キャラクター設定",
"levelConfiguration": "レベル構成",
"characterLevel": "キャラクターレベル",
"max": "最大",
"ultimateEnergy": "アルティメットエネルギー",
"currentEnergy": "現在のエネルギー",
"setTo50": "50%に設定",
"battleConfiguration": "バトル構成",
"useTechnique": "秘技を使用",
"techniqueNote": "バトル前に秘技効果を有効化",
"enhancement": "強化",
"enhancementLevel": "強化レベル",
"origin": "由来",
"enhancedNote": "強化レベルに応じて追加スキル解放",
"lightconeEquipment": "光円錐装備",
"lightconeSettings": "光円錐の設定",
"placeholderLevel": "レベルを入力",
"superimpositionRank": "重ねランク",
"ranksNote": "ランクが高いほど効果強化",
"changeLightcone": "光円錐を変更",
"removeLightcone": "光円錐を外す",
"equipLightcone": "光円錐を装備",
"noLightconeEquipped": "光円錐が装備されていません",
"equipLightconeNote": "強化のため光円錐を装備してください",
"filter": "フィルター",
"selectedCharacters": "選択中のキャラクター",
"selectedProfiles": "選択中のプロファイル",
"clearAll": "全てクリア",
"selectAll": "全て選択",
"copy": "コピー",
"copied": "コピー済み",
"noAvatarSelected": "キャラクターが選択されていません",
"noAvatarToCopySelected": "コピーするキャラクター選択されていません",
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
"pleaseEnterUid": "UIDを入力してください",
"failedToFetchEnkaData": "Enka のデータ取得に失敗",
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
"noDataToImport": "インポートするデータがありません",
"pleaseSelectAFile": "ファイルを選択してください",
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
"importEnkaDataSuccess": "Enka データのインポート成功",
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
"importDatabaseSuccess": "データベースのインポート成功",
"getData": "データ取得",
"import": "インポート",
"freeSRImport": "FreeSR インポート",
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
"pickAFile": "ファイル選択",
"lightConeSetting": "ライトコーン設定",
"relicMaker": "遺物製作",
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
"relicSavedSuccessfully": "遺物の保存に成功しました",
"mainSettings": "メイン設定",
"mainStat": "メインステータス",
"set": "セット",
"pleaseSelectASet": "セットを選択してください",
"effectBonus": "効果ボーナス",
"totalRoll": "総ロール数",
"randomizeStats": "ステータスランダム化",
"randomizeRolls": "ロール回数ランダム化",
"selectASubStat": "サブステータスを選択",
"selectASet": "セットを選択",
"selectAMainStat": "メインステータスを選んでください",
"save": "保存",
"reset": "リセット",
"roll": "ロール",
"step": "ステップ",
"memoryOfChaos": "混沌の記憶",
"pureFiction": "虚構叙事",
"apocalypticShadow": "末日の幻影",
"customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙",
"floor": "階層",
"side": "前半/後半",
"wave": "ウェーブ",
"stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵",
"firstNodeEnemies": "ノード 1 の敵",
"secondNodeEnemies": "ノード 2 の敵",
"thirdNodeEnemies": "ノード 3 の敵",
"firstNode": "ノード 1",
"secondNode": "ノード 2",
"thirdNode": "ノード 3",
"turbulenceBuff": "乱気流バフ",
"noEventSelected": "イベントが選択されていません",
"noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択",
"selectFloor": "階層を選択",
"selectSide": "前半/後半を選択",
"selectBuff": "バフを選択",
"selectStage": "ステージを選択",
"previous": "前へ",
"next": "次へ",
"noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...",
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
"setEffects": "効果を設定",
"details": "詳細",
"normal": "通常攻撃",
"bpskill": "戦闘スキル",
"maze": "秘技",
"ultra": "必殺技",
"servantskill": "メモスプライトスキル",
"severaltalent": "メモスプライトの才能",
"singleattack": "単体攻撃",
"enhance": "強化",
"summon": "召喚",
"blast": "爆発",
"restore": "回復",
"support": "支援",
"aoeattack": "範囲攻撃",
"mazeattack": "迷宮秘技",
"impair": "弱体化",
"bounce": "弾跳",
"active": "アクティブ",
"inactive": "非アクティブ",
"defence": "防御",
"maxAll": "すべて最大化",
"maxAllSuccess": "スキルレベルを最大に設定しました。",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。",
"noRelicEquipped": "遺物が装備されていません",
"anomalyArbitration": "異相の仲裁",
"normalMode": "通常モード",
"hardMode": "困難モード",
"selectPEAKEvent": "PEAK イベントを選択",
"mode": "モード",
"selectMode": "モードを選択",
"rollBack": "前の状態に戻る",
"upRoll": "サブステータスを増やす",
"downRoll": "サブステータスを減らす",
"actions": "アクション",
"avatars": "アバター",
"quickView": "クイックビュー",
"extraSetting": "追加設定",
"disableCensorship": "検閲を無効化",
"hideUI": "UIを非表示",
"theoryCraftMode": "シアリークラフトモード",
"cycleCount": "サイクル数",
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
"theoryCraft": "シアリークラフト",
"multipathCharacter": "複数運命キャラ",
"mainPath": "主人公の運命",
"march7Path": "三月なのかの運命",
"challenge": "挑戦",
"skipNode": "ノードをスキップ",
"disableSkip": "スキップ無効",
"skipNode1": "ノード1をスキップ",
"skipNode2": "ノード2をスキップ",
"extraFeatures": "追加機能",
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
"trailblazer": "開拓者",
"listExtraEffect": "追加効果一覧",
"extra": "追加",
"customLineup": "カスタム編成"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "スキルタイプ",
"skillName": "スキル名",
"character": "キャラクター",
"id": "ID",
"path": "運命",
"rarity": "レアリティ",
"element": "属性",
"technique": "秘技",
"talent": "天賦",
"basic": "通常攻撃",
"skill": "スキル",
"ultimate": "アルティメット",
"servant": "サーバント",
"damage": "ダメージ",
"type": "タイプ",
"warrior": "壊滅",
"knight": "存護",
"mage": "知恵",
"priest": "豊穣",
"rogue": "巡狩",
"shaman": "調和",
"warlock": "虚無",
"memory": "記憶",
"elation": "愉悦",
"fire": "火",
"ice": "氷",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
"wind": "風",
"hp": "HP",
"atk": "攻撃",
"speed": "速度",
"critRate": "会心率",
"critDmg": "会心ダメージ",
"breakEffect": "撃破特効",
"effectRes": "効果抵抗",
"energyRegenerationRate": "EP回復効率",
"effectHitRate": "効果命中率",
"outgoingHealingBoost": "回復増加",
"fireDmgBoost": "火属性ダメージ強化",
"iceDmgBoost": "氷属性ダメージ強化",
"imaginaryDmgBoost": "幻影ダメージ強化",
"physicalDmgBoost": "物理ダメージ強化",
"quantumDmgBoost": "量子ダメージ強化",
"thunderDmgBoost": "雷属性ダメージ強化",
"windDmgBoost": "風属性ダメージ強化",
"pursued": "追加ダメージ",
"true damage": "確定ダメージ",
"elationdamage": "愉悦ダメージ",
"follow-up": "追撃ダメージ",
"elemental damage": "属性ダメージ",
"dot": "継続ダメージ",
"qte": "QTEスキル",
"level": "レベル",
"relics": "遺物",
"eidolons": "エイドロン",
"lightcones": "光円錐",
"loadData": "データ読み込み",
"exportData": "データ出力",
"connectSetting": "接続設定",
"connected": "接続済み",
"unconnected": "未接続",
"psConnection": "PS接続",
"connectionType": "接続タイプ",
"status": "ステータス",
"connectPs": "PS接続",
"disconnect": "切断",
"other": "その他",
"freeSr": "FreeSR",
"database": "データベース",
"enka": "Enka",
"monsterSetting": "モンスター設定",
"serverUrl": "サーバー URL",
"privateType": "プライベートタイプ",
"local": "ローカル",
"server": "サーバー",
"username": "ユーザー名",
"password": "パスワード",
"placeholderServerUrl": "サーバー URL を入力",
"placeholderUsername": "ユーザー名を入力",
"placeholderPassword": "パスワードを入力",
"connectedSuccess": "PS接続成功",
"connectedFailed": "PS接続失敗",
"syncSuccess": "PSへのデータ同期に成功",
"syncFailed": "PSへのデータ同期に失敗",
"sync": "同期",
"importSetting": "インポート設定",
"profile": "プロファイル",
"default": "デフォルト",
"copyProfiles": "プロファイルをコピー",
"addNewProfile": "プロファイルを追加",
"createNewProfile": "新しいプロファイルを作成",
"editProfile": "プロファイル編集",
"placeholderProfileName": "プロファイル名を入力",
"profileName": "プロファイル名",
"create": "作成",
"update": "更新",
"characterInformation": "キャラクター情報",
"skills": "スキル",
"showcaseCard": "ショーケースカード",
"comingSoon": "近日公開",
"characterName": "キャラクター名",
"placeholderCharacter": "キャラクター名を入力",
"characterSettings": "キャラクター設定",
"levelConfiguration": "レベル構成",
"characterLevel": "キャラクターレベル",
"max": "最大",
"ultimateEnergy": "アルティメットエネルギー",
"currentEnergy": "現在のエネルギー",
"setTo50": "50%に設定",
"battleConfiguration": "バトル構成",
"useTechnique": "秘技を使用",
"techniqueNote": "バトル前に秘技効果を有効化",
"enhancement": "強化",
"enhancementLevel": "強化レベル",
"origin": "由来",
"enhancedNote": "強化レベルに応じて追加スキル解放",
"lightconeEquipment": "光円錐装備",
"lightconeSettings": "光円錐の設定",
"placeholderLevel": "レベルを入力",
"superimpositionRank": "重ねランク",
"ranksNote": "ランクが高いほど効果強化",
"changeLightcone": "光円錐を変更",
"removeLightcone": "光円錐を外す",
"equipLightcone": "光円錐を装備",
"noLightconeEquipped": "光円錐が装備されていません",
"equipLightconeNote": "強化のため光円錐を装備してください",
"filter": "フィルター",
"selectedCharacters": "選択中のキャラクター",
"selectedProfiles": "選択中のプロファイル",
"clearAll": "全てクリア",
"selectAll": "全て選択",
"copy": "コピー",
"copied": "コピー済み",
"noAvatarSelected": "キャラクターが選択されていません",
"noAvatarToCopySelected": "コピーするキャラクター選択されていません",
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
"pleaseEnterUid": "UIDを入力してください",
"failedToFetchEnkaData": "Enka のデータ取得に失敗",
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
"noDataToImport": "インポートするデータがありません",
"pleaseSelectAFile": "ファイルを選択してください",
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
"importEnkaDataSuccess": "Enka データのインポート成功",
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
"importDatabaseSuccess": "データベースのインポート成功",
"getData": "データ取得",
"import": "インポート",
"freeSRImport": "FreeSR インポート",
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
"pickAFile": "ファイル選択",
"lightConeSetting": "ライトコーン設定",
"relicMaker": "遺物製作",
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
"relicSavedSuccessfully": "遺物の保存に成功しました",
"mainSettings": "メイン設定",
"mainStat": "メインステータス",
"set": "セット",
"pleaseSelectASet": "セットを選択してください",
"effectBonus": "効果ボーナス",
"totalRoll": "総ロール数",
"randomizeStats": "ステータスランダム化",
"randomizeRolls": "ロール回数ランダム化",
"selectASubStat": "サブステータスを選択",
"selectASet": "セットを選択",
"selectAMainStat": "メインステータスを選んでください",
"save": "保存",
"reset": "リセット",
"roll": "ロール",
"step": "ステップ",
"memoryOfChaos": "混沌の記憶",
"pureFiction": "虚構叙事",
"apocalypticShadow": "末日の幻影",
"customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙",
"floor": "階層",
"side": "前半/後半",
"wave": "ウェーブ",
"stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵",
"firstNodeEnemies": "ノード 1 の敵",
"secondNodeEnemies": "ノード 2 の敵",
"thirdNodeEnemies": "ノード 3 の敵",
"firstNode": "ノード 1",
"secondNode": "ノード 2",
"thirdNode": "ノード 3",
"turbulenceBuff": "乱気流バフ",
"noEventSelected": "イベントが選択されていません",
"noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択",
"selectFloor": "階層を選択",
"selectSide": "前半/後半を選択",
"selectBuff": "バフを選択",
"selectStage": "ステージを選択",
"previous": "前へ",
"next": "次へ",
"noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...",
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
"setEffects": "効果を設定",
"details": "詳細",
"normal": "通常攻撃",
"bpskill": "戦闘スキル",
"maze": "秘技",
"ultra": "必殺技",
"servantskill": "メモスプライトスキル",
"severaltalent": "メモスプライトの才能",
"singleattack": "単体攻撃",
"enhance": "強化",
"summon": "召喚",
"blast": "爆発",
"restore": "回復",
"support": "支援",
"aoeattack": "範囲攻撃",
"mazeattack": "迷宮秘技",
"impair": "弱体化",
"bounce": "弾跳",
"active": "アクティブ",
"inactive": "非アクティブ",
"defence": "防御",
"maxAll": "すべて最大化",
"maxAllSuccess": "スキルレベルを最大に設定しました。",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。",
"noRelicEquipped": "遺物が装備されていません",
"anomalyArbitration": "異相の仲裁",
"normalMode": "通常モード",
"hardMode": "困難モード",
"selectPEAKEvent": "PEAK イベントを選択",
"mode": "モード",
"selectMode": "モードを選択",
"rollBack": "前の状態に戻る",
"upRoll": "サブステータスを増やす",
"downRoll": "サブステータスを減らす",
"actions": "アクション",
"avatars": "アバター",
"quickView": "クイックビュー",
"extraSetting": "追加設定",
"disableCensorship": "検閲を無効化",
"hideUI": "UIを非表示",
"theoryCraftMode": "シアリークラフトモード",
"cycleCount": "サイクル数",
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
"theoryCraft": "シアリークラフト",
"multipathCharacter": "複数運命キャラ",
"mainPath": "主人公の運命",
"march7Path": "三月なのかの運命",
"challenge": "挑戦",
"skipNode": "ノードをスキップ",
"disableSkip": "スキップ無効",
"skipNode1": "ノード1をスキップ",
"skipNode2": "ノード2をスキップ",
"extraFeatures": "追加機能",
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
"trailblazer": "開拓者",
"listExtraEffect": "追加効果一覧",
"extra": "追加",
"customLineup": "カスタム編成"
}
}
+288 -288
View File
@@ -1,289 +1,289 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "스킬 유형",
"skillName": "스킬 이름",
"character": "캐릭터",
"id": "ID",
"path": "운명",
"rarity": "희귀도",
"element": "속성",
"technique": "비기",
"talent": "탈렌트",
"basic": "기본 공격",
"skill": "스킬",
"ultimate": "궁극기",
"servant": "서번트",
"damage": "데미지",
"type": "종류",
"warrior": "파괴",
"knight": "보호",
"mage": "박학",
"priest": "풍요",
"rogue": "사냥",
"shaman": "조화",
"warlock": "허무",
"memory": "기억",
"elation": "환락",
"fire": "화염",
"ice": "얼음",
"imaginary": "환상",
"physical": "물리",
"quantum": "양자",
"thunder": "번개",
"wind": "바람",
"hp": "HP",
"atk": "공격력",
"speed": "속도",
"critRate": "치명타 확률",
"critDmg": "치명타 피해",
"breakEffect": "파괴 피해",
"effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복속도",
"effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유 강화",
"fireDmgBoost": "화염 피해 증가",
"iceDmgBoost": "얼음 피해 증가",
"imaginaryDmgBoost": "환상 피해 증가",
"physicalDmgBoost": "물리 피해 증가",
"quantumDmgBoost": "양자 피해 증가",
"thunderDmgBoost": "번개 피해 증가",
"windDmgBoost": "바람 피해 증가",
"pursued": "추가 피해",
"true damage": "진실한 피해",
"elationdamage": "환락 피해",
"follow-up": "후속 피해",
"elemental damage": "파괴 및 초파괴 피해",
"dot": "시간 경과 피해",
"qte": "QTE 스킬",
"level": "레벨",
"relics": "유물",
"eidolons": "에이돌론",
"lightcones": "라이트콘",
"loadData": "데이터 불러오기",
"exportData": "데이터 내보내기",
"connectSetting": "연결 설정",
"connected": "연결됨",
"unconnected": "연결 안됨",
"psConnection": "PS 연결",
"connectionType": "연결 타입",
"status": "상태",
"connectPs": "PS 연결",
"disconnect": "연결 끊기",
"other": "기타",
"freeSr": "FreeSR",
"database": "데이터베이스",
"enka": "Enka",
"monsterSetting": "몬스터 설정",
"serverUrl": "서버 URL",
"privateType": "프라이빗 타입",
"local": "로컬",
"server": "서버",
"username": "사용자명",
"password": "비밀번호",
"placeholderServerUrl": "서버 URL 입력",
"placeholderUsername": "사용자명 입력",
"placeholderPassword": "비밀번호 입력",
"connectedSuccess": "PS에 성공적으로 연결됨",
"connectedFailed": "PS 연결 실패",
"syncSuccess": "PS에 데이터 동기화 성공",
"syncFailed": "PS에 데이터 동기화 실패",
"sync": "동기화",
"importSetting": "설정 가져오기",
"profile": "프로필",
"default": "기본",
"copyProfiles": "프로필 복사",
"addNewProfile": "새 프로필 추가",
"createNewProfile": "새 프로필 생성",
"editProfile": "프로필 편집",
"placeholderProfileName": "프로필 이름 입력",
"profileName": "프로필 이름",
"create": "생성",
"update": "업데이트",
"characterInformation": "캐릭터 정보",
"skills": "스킬",
"showcaseCard": "쇼케이스 카드",
"comingSoon": "곧 출시",
"characterName": "캐릭터 이름",
"placeholderCharacter": "캐릭터 이름 입력",
"characterSettings": "캐릭터 설정",
"levelConfiguration": "레벨 구성",
"characterLevel": "캐릭터 레벨",
"max": "최대",
"ultimateEnergy": "궁극 에너지",
"currentEnergy": "현재 에너지",
"setTo50": "50%로 설정",
"battleConfiguration": "전투 구성",
"useTechnique": "비기 사용",
"techniqueNote": "전투 전 비기 효과 활성화",
"enhancement": "강화",
"enhancementLevel": "강화 레벨",
"origin": "출처",
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
"lightconeEquipment": "라이트콘 장비",
"lightconeSettings": "라이트콘 설정",
"placeholderLevel": "레벨 입력",
"superimpositionRank": "중첩 등급",
"ranksNote": "등급이 높을수록 효과가 강함",
"changeLightcone": "라이트콘 변경",
"removeLightcone": "라이트콘 제거",
"equipLightcone": "라이트콘 장착",
"noLightconeEquipped": "라이트콘 미장착",
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
"filter": "필터",
"selectedCharacters": "선택된 캐릭터",
"selectedProfiles": "선택된 프로필",
"clearAll": "전체 지우기",
"selectAll": "전체 선택",
"copy": "복사",
"copied": "복사됨",
"noAvatarSelected": "캐릭터가 선택되지 않음",
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
"pleaseEnterUid": "UID를 입력해주세요",
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
"noDataToImport": "가져올 데이터가 없습니다",
"pleaseSelectAFile": "파일을 선택하세요",
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
"getData": "데이터 가져오기",
"import": "가져오기",
"freeSRImport": "FreeSR 가져오기",
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
"pickAFile": "파일 선택",
"lightConeSetting": "라이트콘 설정",
"relicMaker": "유물 제작",
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
"relicSavedSuccessfully": "유물 저장 성공",
"mainSettings": "메인 설정",
"mainStat": "주 속성",
"set": "세트",
"pleaseSelectASet": "세트 하나를 선택해주세요",
"effectBonus": "효과 보너스",
"totalRoll": "총 횟수",
"randomizeStats": "속성 랜덤화",
"randomizeRolls": "횟수 랜덤화",
"selectASubStat": "부 속성 선택",
"selectASet": "세트 선택",
"selectAMainStat": "주 속성 선택",
"save": "저장",
"reset": "초기화",
"roll": "굴리기",
"step": "단계",
"memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수",
"side": "전반/후반",
"wave": "웨이브",
"stage": "스테이지",
"useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적",
"firstNodeEnemies": "노드 1 적",
"secondNodeEnemies": "노드 2 적",
"thirdNodeEnemies": "노드 3 적",
"firstNode": "노드 1",
"secondNode": "노드 2",
"thirdNode": "노드 3",
"turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반",
"lower": "하반",
"upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택",
"selectFloor": "층 선택",
"selectSide": "전반/후반 선택",
"selectBuff": "버프 선택",
"selectStage": "스테이지 선택",
"previous": "이전",
"next": "다음",
"noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...",
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정",
"details": "상세",
"normal": "기본 공격",
"bpskill": "스킬",
"maze": "전술",
"ultra": "필살기",
"servantskill": "메모스프라이트 스킬",
"severaltalent": "메모스프라이트 재능",
"singleattack": "단일 공격",
"enhance": "강화",
"summon": "소환",
"blast": "광역 공격",
"restore": "회복",
"support": "지원",
"aoeattack": "광역 공격",
"mazeattack": "미궁 비기",
"impair": "약화",
"bounce": "튕김",
"active": "활성",
"inactive": "비활성",
"defence": "방어",
"maxAll": "모두 최대치",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
"noRelicEquipped": "성유물이 장착되지 않음",
"anomalyArbitration": "이상 중재",
"normalMode": "일반 모드",
"hardMode": "어려움 모드",
"selectPEAKEvent": "PEAK 이벤트 선택",
"mode": "모드",
"selectMode": "모드를 선택",
"rollBack": "이전 상태로 되돌리기",
"upRoll": "부옵션 추가",
"downRoll": "부옵션 감소",
"actions": "동작",
"avatars": "아바타",
"quickView": "빠른 조회",
"extraSetting": "추가 설정",
"disableCensorship": "검열 비활성화",
"hideUI": "UI 숨기기",
"theoryCraftMode": "Theory Craft 모드",
"cycleCount": "사이클 수",
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
"theoryCraft": "Theory Craft",
"multipathCharacter": "다중 운명 캐릭터",
"mainPath": "개척자 운명",
"march7Path": "삼월칠일 운명",
"challenge": "도전",
"skipNode": "노드 건너뛰기",
"disableSkip": "건너뛰기 비활성화",
"skipNode1": "노드 1 건너뛰기",
"skipNode2": "노드 2 건너뛰기",
"extraFeatures": "추가 기능",
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
"trailblazer": "개척자",
"listExtraEffect": "추가 효과 목록",
"extra": "추가",
"customLineup": "커스텀 편성"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "스킬 유형",
"skillName": "스킬 이름",
"character": "캐릭터",
"id": "ID",
"path": "운명",
"rarity": "희귀도",
"element": "속성",
"technique": "비기",
"talent": "탈렌트",
"basic": "기본 공격",
"skill": "스킬",
"ultimate": "궁극기",
"servant": "서번트",
"damage": "데미지",
"type": "종류",
"warrior": "파괴",
"knight": "보호",
"mage": "박학",
"priest": "풍요",
"rogue": "사냥",
"shaman": "조화",
"warlock": "허무",
"memory": "기억",
"elation": "환락",
"fire": "화염",
"ice": "얼음",
"imaginary": "환상",
"physical": "물리",
"quantum": "양자",
"thunder": "번개",
"wind": "바람",
"hp": "HP",
"atk": "공격력",
"speed": "속도",
"critRate": "치명타 확률",
"critDmg": "치명타 피해",
"breakEffect": "파괴 피해",
"effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복속도",
"effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유 강화",
"fireDmgBoost": "화염 피해 증가",
"iceDmgBoost": "얼음 피해 증가",
"imaginaryDmgBoost": "환상 피해 증가",
"physicalDmgBoost": "물리 피해 증가",
"quantumDmgBoost": "양자 피해 증가",
"thunderDmgBoost": "번개 피해 증가",
"windDmgBoost": "바람 피해 증가",
"pursued": "추가 피해",
"true damage": "진실한 피해",
"elationdamage": "환락 피해",
"follow-up": "후속 피해",
"elemental damage": "파괴 및 초파괴 피해",
"dot": "시간 경과 피해",
"qte": "QTE 스킬",
"level": "레벨",
"relics": "유물",
"eidolons": "에이돌론",
"lightcones": "라이트콘",
"loadData": "데이터 불러오기",
"exportData": "데이터 내보내기",
"connectSetting": "연결 설정",
"connected": "연결됨",
"unconnected": "연결 안됨",
"psConnection": "PS 연결",
"connectionType": "연결 타입",
"status": "상태",
"connectPs": "PS 연결",
"disconnect": "연결 끊기",
"other": "기타",
"freeSr": "FreeSR",
"database": "데이터베이스",
"enka": "Enka",
"monsterSetting": "몬스터 설정",
"serverUrl": "서버 URL",
"privateType": "프라이빗 타입",
"local": "로컬",
"server": "서버",
"username": "사용자명",
"password": "비밀번호",
"placeholderServerUrl": "서버 URL 입력",
"placeholderUsername": "사용자명 입력",
"placeholderPassword": "비밀번호 입력",
"connectedSuccess": "PS에 성공적으로 연결됨",
"connectedFailed": "PS 연결 실패",
"syncSuccess": "PS에 데이터 동기화 성공",
"syncFailed": "PS에 데이터 동기화 실패",
"sync": "동기화",
"importSetting": "설정 가져오기",
"profile": "프로필",
"default": "기본",
"copyProfiles": "프로필 복사",
"addNewProfile": "새 프로필 추가",
"createNewProfile": "새 프로필 생성",
"editProfile": "프로필 편집",
"placeholderProfileName": "프로필 이름 입력",
"profileName": "프로필 이름",
"create": "생성",
"update": "업데이트",
"characterInformation": "캐릭터 정보",
"skills": "스킬",
"showcaseCard": "쇼케이스 카드",
"comingSoon": "곧 출시",
"characterName": "캐릭터 이름",
"placeholderCharacter": "캐릭터 이름 입력",
"characterSettings": "캐릭터 설정",
"levelConfiguration": "레벨 구성",
"characterLevel": "캐릭터 레벨",
"max": "최대",
"ultimateEnergy": "궁극 에너지",
"currentEnergy": "현재 에너지",
"setTo50": "50%로 설정",
"battleConfiguration": "전투 구성",
"useTechnique": "비기 사용",
"techniqueNote": "전투 전 비기 효과 활성화",
"enhancement": "강화",
"enhancementLevel": "강화 레벨",
"origin": "출처",
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
"lightconeEquipment": "라이트콘 장비",
"lightconeSettings": "라이트콘 설정",
"placeholderLevel": "레벨 입력",
"superimpositionRank": "중첩 등급",
"ranksNote": "등급이 높을수록 효과가 강함",
"changeLightcone": "라이트콘 변경",
"removeLightcone": "라이트콘 제거",
"equipLightcone": "라이트콘 장착",
"noLightconeEquipped": "라이트콘 미장착",
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
"filter": "필터",
"selectedCharacters": "선택된 캐릭터",
"selectedProfiles": "선택된 프로필",
"clearAll": "전체 지우기",
"selectAll": "전체 선택",
"copy": "복사",
"copied": "복사됨",
"noAvatarSelected": "캐릭터가 선택되지 않음",
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
"pleaseEnterUid": "UID를 입력해주세요",
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
"noDataToImport": "가져올 데이터가 없습니다",
"pleaseSelectAFile": "파일을 선택하세요",
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
"getData": "데이터 가져오기",
"import": "가져오기",
"freeSRImport": "FreeSR 가져오기",
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
"pickAFile": "파일 선택",
"lightConeSetting": "라이트콘 설정",
"relicMaker": "유물 제작",
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
"relicSavedSuccessfully": "유물 저장 성공",
"mainSettings": "메인 설정",
"mainStat": "주 속성",
"set": "세트",
"pleaseSelectASet": "세트 하나를 선택해주세요",
"effectBonus": "효과 보너스",
"totalRoll": "총 횟수",
"randomizeStats": "속성 랜덤화",
"randomizeRolls": "횟수 랜덤화",
"selectASubStat": "부 속성 선택",
"selectASet": "세트 선택",
"selectAMainStat": "주 속성 선택",
"save": "저장",
"reset": "초기화",
"roll": "굴리기",
"step": "단계",
"memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수",
"side": "전반/후반",
"wave": "웨이브",
"stage": "스테이지",
"useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적",
"firstNodeEnemies": "노드 1 적",
"secondNodeEnemies": "노드 2 적",
"thirdNodeEnemies": "노드 3 적",
"firstNode": "노드 1",
"secondNode": "노드 2",
"thirdNode": "노드 3",
"turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반",
"lower": "하반",
"upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택",
"selectFloor": "층 선택",
"selectSide": "전반/후반 선택",
"selectBuff": "버프 선택",
"selectStage": "스테이지 선택",
"previous": "이전",
"next": "다음",
"noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...",
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정",
"details": "상세",
"normal": "기본 공격",
"bpskill": "스킬",
"maze": "전술",
"ultra": "필살기",
"servantskill": "메모스프라이트 스킬",
"severaltalent": "메모스프라이트 재능",
"singleattack": "단일 공격",
"enhance": "강화",
"summon": "소환",
"blast": "광역 공격",
"restore": "회복",
"support": "지원",
"aoeattack": "광역 공격",
"mazeattack": "미궁 비기",
"impair": "약화",
"bounce": "튕김",
"active": "활성",
"inactive": "비활성",
"defence": "방어",
"maxAll": "모두 최대치",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
"noRelicEquipped": "성유물이 장착되지 않음",
"anomalyArbitration": "이상 중재",
"normalMode": "일반 모드",
"hardMode": "어려움 모드",
"selectPEAKEvent": "PEAK 이벤트 선택",
"mode": "모드",
"selectMode": "모드를 선택",
"rollBack": "이전 상태로 되돌리기",
"upRoll": "부옵션 추가",
"downRoll": "부옵션 감소",
"actions": "동작",
"avatars": "아바타",
"quickView": "빠른 조회",
"extraSetting": "추가 설정",
"disableCensorship": "검열 비활성화",
"hideUI": "UI 숨기기",
"theoryCraftMode": "Theory Craft 모드",
"cycleCount": "사이클 수",
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
"theoryCraft": "Theory Craft",
"multipathCharacter": "다중 운명 캐릭터",
"mainPath": "개척자 운명",
"march7Path": "삼월칠일 운명",
"challenge": "도전",
"skipNode": "노드 건너뛰기",
"disableSkip": "건너뛰기 비활성화",
"skipNode1": "노드 1 건너뛰기",
"skipNode2": "노드 2 건너뛰기",
"extraFeatures": "추가 기능",
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
"trailblazer": "개척자",
"listExtraEffect": "추가 효과 목록",
"extra": "추가",
"customLineup": "커스텀 편성"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Ferramentas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de Habilidade",
"skillName": "Nome da Habilidade",
"character": "Personagem",
"id": "Id",
"path": "Caminho",
"rarity": "Raridade",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Perícia",
"ultimate": "Perícia Suprema",
"servant": "Servo",
"damage": "Dano",
"type": "Tipo",
"warrior": "A Destruição",
"knight": "A Preservação",
"mage": "A Erudição",
"priest": "A Abundância",
"rogue": "A Caça",
"shaman": "A Harmonia",
"warlock": "A Inexistência",
"memory": "A Lembrança",
"elation": "A Alegria",
"fire": "Fogo",
"ice": "Gelo",
"imaginary": "Imaginário",
"physical": "Físico",
"quantum": "Quântico",
"thunder": "Raio",
"wind": "Vento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Taxa CRIT",
"critDmg": "Dano CRIT",
"breakEffect": "Efeito de Quebra",
"effectRes": "RES a Efeito",
"energyRegenerationRate": "Taxa de Regeneração de Energia",
"effectHitRate": "Taxa de Acerto de Efeito",
"outgoingHealingBoost": "Bônus de Cura",
"fireDmgBoost": "Bônus de Dano de Fogo",
"iceDmgBoost": "Bônus de Dano de Gelo",
"imaginaryDmgBoost": "Bônus de Dano Imaginário",
"physicalDmgBoost": "Bônus de Dano Físico",
"quantumDmgBoost": "Bônus de Dano Quântico",
"thunderDmgBoost": "Bônus de Dano de Raio",
"windDmgBoost": "Bônus de Dano de Vento",
"pursued": "Dano Adicional",
"true damage": "Dano Verdadeiro",
"elationdamage": "Dano de Alegria",
"follow-up": "Dano de Ataque Extra",
"elemental damage": "Dano de Quebra e Superquebra",
"dot": "Dano Contínuo",
"qte": "Habilidade QTE",
"level": "Nível",
"relics": "Relíquias",
"eidolons": "Eidolons",
"lightcones": "Cones de Luz",
"loadData": "Carregar dados",
"exportData": "Exportar dados",
"connectSetting": "Configuração de Conexão",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexão PS",
"connectionType": "Tipo de Conexão",
"status": "Status",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Outro",
"freeSr": "FreeSR",
"database": "Banco de dados",
"enka": "Enka",
"monsterSetting": "Configuração de Monstros",
"serverUrl": "URL do Servidor",
"privateType": "Tipo Privado",
"local": "Local",
"server": "Servidor",
"username": "Usuário",
"password": "Senha",
"placeholderServerUrl": "Digite o URL do servidor",
"placeholderUsername": "Digite o usuário",
"placeholderPassword": "Digite a senha",
"connectedSuccess": "Conectado ao PS com sucesso",
"connectedFailed": "Falha ao conectar ao PS",
"syncSuccess": "Dados sincronizados com o PS com sucesso",
"syncFailed": "Falha ao sincronizar dados com o PS",
"sync": "Sincronizar",
"importSetting": "Configuração de Importação",
"profile": "Perfil",
"default": "Padrão",
"copyProfiles": "Copiar perfis",
"addNewProfile": "Adicionar novo perfil",
"createNewProfile": "Criar novo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Digite o nome do perfil",
"profileName": "Nome do perfil",
"create": "Criar",
"update": "Atualizar",
"characterInformation": "Informação do Personagem",
"skills": "Habilidades",
"showcaseCard": "Cartão de Exibição",
"comingSoon": "Em breve",
"characterName": "Nome do personagem",
"placeholderCharacter": "Digite o nome do personagem",
"characterSettings": "Configurações de Personagem",
"levelConfiguration": "Configuração de Nível",
"characterLevel": "Nível do Personagem",
"max": "MÁX",
"ultimateEnergy": "Energia da Perícia Suprema",
"currentEnergy": "Energia Atual",
"setTo50": "Definir para 50%",
"battleConfiguration": "Configuração de Batalha",
"useTechnique": "Usar Técnica",
"techniqueNote": "Ativar efeitos de técnica pré-batalha",
"enhancement": "Aprimoramento",
"enhancementLevel": "Nível de Aprimoramento",
"origin": "Origem",
"enhancedNote": "Níveis altos desbloqueiam habilidades",
"lightconeEquipment": "Equipamento de Cone de Luz",
"lightconeSettings": "Configurações de Cone de Luz",
"placeholderLevel": "Digite o nível",
"superimpositionRank": "Rank de Sobreposição",
"ranksNote": "Ranks maiores dão efeitos mais fortes",
"changeLightcone": "Mudar Cone de Luz",
"removeLightcone": "Remover Cone de Luz",
"equipLightcone": "Equipar Cone de Luz",
"noLightconeEquipped": "Sem Cone de Luz",
"equipLightconeNote": "Equipe um cone de luz para melhorar o personagem",
"filter": "Filtro",
"selectedCharacters": "Personagens Selecionados",
"selectedProfiles": "Perfis Selecionados",
"clearAll": "Limpar Tudo",
"selectAll": "Selecionar Tudo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Nenhum personagem selecionado",
"noAvatarToCopySelected": "Nenhum personagem para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil",
"pleaseEnterUid": "Por favor, insira o UID",
"failedToFetchEnkaData": "Falha ao obter dados Enka",
"pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem",
"noDataToImport": "Sem dados para importar",
"pleaseSelectAFile": "Por favor, selecione um arquivo",
"fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido",
"importEnkaDataSuccess": "Dados Enka importados com sucesso",
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
"importDatabaseSuccess": "Banco de dados importado com sucesso",
"getData": "Obter Dados",
"import": "Importar",
"freeSRImport": "Importação FreeSR",
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
"pickAFile": "Escolher um arquivo",
"lightConeSetting": "Configuração de Cone de Luz",
"relicMaker": "Criador de Relíquias",
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
"relicSavedSuccessfully": "Relíquia salva com sucesso",
"mainSettings": "Configurações Principais",
"mainStat": "Atributo Principal",
"set": "Conjunto",
"pleaseSelectASet": "Selecione um conjunto",
"effectBonus": "Bônus de Efeito",
"totalRoll": "Total de Melhorias",
"randomizeStats": "Atributos Aleatórios",
"randomizeRolls": "Melhorias Aleatórias",
"selectASubStat": "Selecionar sub-atributo",
"selectASet": "Selecionar conjunto",
"selectAMainStat": "Selecionar atributo principal",
"save": "Salvar",
"reset": "Redefinir",
"roll": "Melhoria",
"step": "Passo",
"memoryOfChaos": "Memória do Caos",
"pureFiction": "Pura Ficção",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Inimigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Andar",
"side": "Lado",
"wave": "Onda",
"stage": "Fase",
"useCycleCount": "Usar contagem de ciclos?",
"useTurbulenceBuff": "Usar buff de turbulência?",
"firstHalfEnemies": "Inimigos da primeira metade",
"secondHalfEnemies": "Inimigos da segunda metade",
"firstNodeEnemies": "Inimigos do nodo 1",
"secondNodeEnemies": "Inimigos do nodo 2",
"thirdNodeEnemies": "Inimigos do nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de inimigos",
"turbulenceBuff": "Buff de Turbulência",
"noEventSelected": "Nenhum evento selecionado",
"noTurbulenceBuff": "Sem Buff de Turbulência",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Selecionar Evento MOC",
"selectPFEvent": "Selecionar Evento PF",
"selectASEvent": "Selecionar Evento AS",
"selectCEEvent": "Selecionar Evento CE",
"selectEvent": "Selecionar Evento",
"selectFloor": "Selecionar Andar",
"selectSide": "Selecionar Lado",
"selectBuff": "Selecionar Buff",
"selectStage": "Selecionar Fase",
"previous": "Anterior",
"next": "Próximo",
"noMonstersFound": "Nenhum monstro encontrado",
"addNewWave": "Adicionar Nova Onda",
"searchStage": "Buscar fase...",
"noStageFound": "Fase não encontrada",
"searchMonster": "Buscar monstro...",
"changeRelic": "Mudar relíquia",
"deleteRelic": "Deletar relíquia",
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
"setEffects": "Definir Efeitos",
"details": "Detalhes",
"normal": "Ataque Básico",
"bpskill": "Perícia",
"maze": "Técnica",
"ultra": "Perícia Suprema",
"servantskill": "Habilidade do Memoespirito",
"severaltalent": "Talento do Memoespirito",
"singleattack": "Ataque Único",
"enhance": "Aprimorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Explosão",
"restore": "Restaurar",
"support": "Suporte",
"aoeattack": "Ataque em Área",
"impair": "Enfraquecer",
"bounce": "Rebote",
"active": "Ativo",
"defence": "Defesa",
"inactive": "Inativo",
"maxAll": "Maximizar Tudo",
"maxAllSuccess": "Habilidades maximizadas com sucesso.",
"maxAllFailed": "Falha ao maximizar habilidades.",
"noRelicEquipped": "Sem relíquia equipada",
"anomalyArbitration": "Arbitragem de Anomalia",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Selecionar Evento PEAK",
"mode": "Modo",
"selectMode": "Selecionar um modo",
"rollBack": "Reverter",
"upRoll": "Aumentar Melhoria",
"downRoll": "Diminuir Melhoria",
"actions": "Ações",
"avatars": "Avatares",
"quickView": "Visualização Rápida",
"extraSetting": "Configurações Extras",
"disableCensorship": "Desativar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Contagem de Ciclos",
"pleaseSelectAllSubStats": "Selecione todos os sub-atributos",
"subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personagem Multicaminho",
"mainPath": "Caminho Principal",
"march7Path": "Caminho de 7 de Março",
"challenge": "Desafio",
"skipNode": "Pular Nó",
"disableSkip": "Desativar pulo",
"skipNode1": "Pular nó 1",
"skipNode2": "Pular nó 2",
"extraFeatures": "Recursos Extras",
"detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.",
"detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.",
"detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.",
"detailHiddenUi": "Irá esconder a interface do jogo.",
"detailDisableCensorship": "Desativa a censura no jogo.",
"detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.",
"trailblazer": "Desbravador",
"listExtraEffect": "Lista de Efeitos Extras",
"extra": "Extra",
"customLineup": "Formação Personalizada"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Ferramentas Firefly por Firefly Shelter"
},
"DataPage": {
"skillType": "Tipo de Habilidade",
"skillName": "Nome da Habilidade",
"character": "Personagem",
"id": "Id",
"path": "Caminho",
"rarity": "Raridade",
"element": "Elemento",
"technique": "Técnica",
"talent": "Talento",
"basic": "Ataque Básico",
"skill": "Perícia",
"ultimate": "Perícia Suprema",
"servant": "Servo",
"damage": "Dano",
"type": "Tipo",
"warrior": "A Destruição",
"knight": "A Preservação",
"mage": "A Erudição",
"priest": "A Abundância",
"rogue": "A Caça",
"shaman": "A Harmonia",
"warlock": "A Inexistência",
"memory": "A Lembrança",
"elation": "A Alegria",
"fire": "Fogo",
"ice": "Gelo",
"imaginary": "Imaginário",
"physical": "Físico",
"quantum": "Quântico",
"thunder": "Raio",
"wind": "Vento",
"hp": "PV",
"atk": "ATQ",
"speed": "VEL",
"critRate": "Taxa CRIT",
"critDmg": "Dano CRIT",
"breakEffect": "Efeito de Quebra",
"effectRes": "RES a Efeito",
"energyRegenerationRate": "Taxa de Regeneração de Energia",
"effectHitRate": "Taxa de Acerto de Efeito",
"outgoingHealingBoost": "Bônus de Cura",
"fireDmgBoost": "Bônus de Dano de Fogo",
"iceDmgBoost": "Bônus de Dano de Gelo",
"imaginaryDmgBoost": "Bônus de Dano Imaginário",
"physicalDmgBoost": "Bônus de Dano Físico",
"quantumDmgBoost": "Bônus de Dano Quântico",
"thunderDmgBoost": "Bônus de Dano de Raio",
"windDmgBoost": "Bônus de Dano de Vento",
"pursued": "Dano Adicional",
"true damage": "Dano Verdadeiro",
"elationdamage": "Dano de Alegria",
"follow-up": "Dano de Ataque Extra",
"elemental damage": "Dano de Quebra e Superquebra",
"dot": "Dano Contínuo",
"qte": "Habilidade QTE",
"level": "Nível",
"relics": "Relíquias",
"eidolons": "Eidolons",
"lightcones": "Cones de Luz",
"loadData": "Carregar dados",
"exportData": "Exportar dados",
"connectSetting": "Configuração de Conexão",
"connected": "Conectado",
"unconnected": "Desconectado",
"psConnection": "Conexão PS",
"connectionType": "Tipo de Conexão",
"status": "Status",
"connectPs": "Conectar PS",
"disconnect": "Desconectar",
"other": "Outro",
"freeSr": "FreeSR",
"database": "Banco de dados",
"enka": "Enka",
"monsterSetting": "Configuração de Monstros",
"serverUrl": "URL do Servidor",
"privateType": "Tipo Privado",
"local": "Local",
"server": "Servidor",
"username": "Usuário",
"password": "Senha",
"placeholderServerUrl": "Digite o URL do servidor",
"placeholderUsername": "Digite o usuário",
"placeholderPassword": "Digite a senha",
"connectedSuccess": "Conectado ao PS com sucesso",
"connectedFailed": "Falha ao conectar ao PS",
"syncSuccess": "Dados sincronizados com o PS com sucesso",
"syncFailed": "Falha ao sincronizar dados com o PS",
"sync": "Sincronizar",
"importSetting": "Configuração de Importação",
"profile": "Perfil",
"default": "Padrão",
"copyProfiles": "Copiar perfis",
"addNewProfile": "Adicionar novo perfil",
"createNewProfile": "Criar novo perfil",
"editProfile": "Editar perfil",
"placeholderProfileName": "Digite o nome do perfil",
"profileName": "Nome do perfil",
"create": "Criar",
"update": "Atualizar",
"characterInformation": "Informação do Personagem",
"skills": "Habilidades",
"showcaseCard": "Cartão de Exibição",
"comingSoon": "Em breve",
"characterName": "Nome do personagem",
"placeholderCharacter": "Digite o nome do personagem",
"characterSettings": "Configurações de Personagem",
"levelConfiguration": "Configuração de Nível",
"characterLevel": "Nível do Personagem",
"max": "MÁX",
"ultimateEnergy": "Energia da Perícia Suprema",
"currentEnergy": "Energia Atual",
"setTo50": "Definir para 50%",
"battleConfiguration": "Configuração de Batalha",
"useTechnique": "Usar Técnica",
"techniqueNote": "Ativar efeitos de técnica pré-batalha",
"enhancement": "Aprimoramento",
"enhancementLevel": "Nível de Aprimoramento",
"origin": "Origem",
"enhancedNote": "Níveis altos desbloqueiam habilidades",
"lightconeEquipment": "Equipamento de Cone de Luz",
"lightconeSettings": "Configurações de Cone de Luz",
"placeholderLevel": "Digite o nível",
"superimpositionRank": "Rank de Sobreposição",
"ranksNote": "Ranks maiores dão efeitos mais fortes",
"changeLightcone": "Mudar Cone de Luz",
"removeLightcone": "Remover Cone de Luz",
"equipLightcone": "Equipar Cone de Luz",
"noLightconeEquipped": "Sem Cone de Luz",
"equipLightconeNote": "Equipe um cone de luz para melhorar o personagem",
"filter": "Filtro",
"selectedCharacters": "Personagens Selecionados",
"selectedProfiles": "Perfis Selecionados",
"clearAll": "Limpar Tudo",
"selectAll": "Selecionar Tudo",
"copy": "Copiar",
"copied": "Copiado",
"noAvatarSelected": "Nenhum personagem selecionado",
"noAvatarToCopySelected": "Nenhum personagem para copiar",
"pleaseSelectAtLeastOneProfile": "Por favor, selecione ao menos um perfil",
"pleaseEnterUid": "Por favor, insira o UID",
"failedToFetchEnkaData": "Falha ao obter dados Enka",
"pleaseSelectAtLeastOneCharacter": "Selecione ao menos um personagem",
"noDataToImport": "Sem dados para importar",
"pleaseSelectAFile": "Por favor, selecione um arquivo",
"fileMustBeAValidJsonFile": "O arquivo deve ser um JSON válido",
"importEnkaDataSuccess": "Dados Enka importados com sucesso",
"importFreeSRDataSuccess": "Dados FreeSR importados com sucesso",
"importDatabaseSuccess": "Banco de dados importado com sucesso",
"getData": "Obter Dados",
"import": "Importar",
"freeSRImport": "Importação FreeSR",
"onlySupportFreeSRJsonFile": "Suporta apenas arquivos JSON FreeSR",
"pickAFile": "Escolher um arquivo",
"lightConeSetting": "Configuração de Cone de Luz",
"relicMaker": "Criador de Relíquias",
"pleaseSelectAllOptions": "Por favor selecione todas as opções",
"relicSavedSuccessfully": "Relíquia salva com sucesso",
"mainSettings": "Configurações Principais",
"mainStat": "Atributo Principal",
"set": "Conjunto",
"pleaseSelectASet": "Selecione um conjunto",
"effectBonus": "Bônus de Efeito",
"totalRoll": "Total de Melhorias",
"randomizeStats": "Atributos Aleatórios",
"randomizeRolls": "Melhorias Aleatórias",
"selectASubStat": "Selecionar sub-atributo",
"selectASet": "Selecionar conjunto",
"selectAMainStat": "Selecionar atributo principal",
"save": "Salvar",
"reset": "Redefinir",
"roll": "Melhoria",
"step": "Passo",
"memoryOfChaos": "Memória do Caos",
"pureFiction": "Pura Ficção",
"apocalypticShadow": "Sombra Apocalíptica",
"customEnemy": "Inimigo Personalizado",
"simulatedUniverse": "Universo Simulado",
"floor": "Andar",
"side": "Lado",
"wave": "Onda",
"stage": "Fase",
"useCycleCount": "Usar contagem de ciclos?",
"useTurbulenceBuff": "Usar buff de turbulência?",
"firstHalfEnemies": "Inimigos da primeira metade",
"secondHalfEnemies": "Inimigos da segunda metade",
"firstNodeEnemies": "Inimigos do nodo 1",
"secondNodeEnemies": "Inimigos do nodo 2",
"thirdNodeEnemies": "Inimigos do nodo 3",
"firstNode": "Nodo 1",
"secondNode": "Nodo 2",
"thirdNode": "Nodo 3",
"listEnemies": "Lista de inimigos",
"turbulenceBuff": "Buff de Turbulência",
"noEventSelected": "Nenhum evento selecionado",
"noTurbulenceBuff": "Sem Buff de Turbulência",
"upper": "Superior",
"lower": "Inferior",
"upperToLower": "Superior -> Inferior",
"lowerToUpper": "Inferior -> Superior",
"selectMOCEvent": "Selecionar Evento MOC",
"selectPFEvent": "Selecionar Evento PF",
"selectASEvent": "Selecionar Evento AS",
"selectCEEvent": "Selecionar Evento CE",
"selectEvent": "Selecionar Evento",
"selectFloor": "Selecionar Andar",
"selectSide": "Selecionar Lado",
"selectBuff": "Selecionar Buff",
"selectStage": "Selecionar Fase",
"previous": "Anterior",
"next": "Próximo",
"noMonstersFound": "Nenhum monstro encontrado",
"addNewWave": "Adicionar Nova Onda",
"searchStage": "Buscar fase...",
"noStageFound": "Fase não encontrada",
"searchMonster": "Buscar monstro...",
"changeRelic": "Mudar relíquia",
"deleteRelic": "Deletar relíquia",
"deleteRelicConfirm": "Tem certeza que deseja deletar a relíquia neste slot?",
"setEffects": "Definir Efeitos",
"details": "Detalhes",
"normal": "Ataque Básico",
"bpskill": "Perícia",
"maze": "Técnica",
"ultra": "Perícia Suprema",
"servantskill": "Habilidade do Memoespirito",
"severaltalent": "Talento do Memoespirito",
"singleattack": "Ataque Único",
"enhance": "Aprimorar",
"summon": "Invocar",
"mazeattack": "Ataque de Técnica",
"blast": "Explosão",
"restore": "Restaurar",
"support": "Suporte",
"aoeattack": "Ataque em Área",
"impair": "Enfraquecer",
"bounce": "Rebote",
"active": "Ativo",
"defence": "Defesa",
"inactive": "Inativo",
"maxAll": "Maximizar Tudo",
"maxAllSuccess": "Habilidades maximizadas com sucesso.",
"maxAllFailed": "Falha ao maximizar habilidades.",
"noRelicEquipped": "Sem relíquia equipada",
"anomalyArbitration": "Arbitragem de Anomalia",
"normalMode": "Modo Normal",
"hardMode": "Modo Difícil",
"selectPEAKEvent": "Selecionar Evento PEAK",
"mode": "Modo",
"selectMode": "Selecionar um modo",
"rollBack": "Reverter",
"upRoll": "Aumentar Melhoria",
"downRoll": "Diminuir Melhoria",
"actions": "Ações",
"avatars": "Avatares",
"quickView": "Visualização Rápida",
"extraSetting": "Configurações Extras",
"disableCensorship": "Desativar Censura",
"hideUI": "Ocultar UI",
"theoryCraftMode": "Modo Theorycraft",
"cycleCount": "Contagem de Ciclos",
"pleaseSelectAllSubStats": "Selecione todos os sub-atributos",
"subStatRollCountCannotBeZero": "As melhorias do sub-atributo não podem ser zero",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Personagem Multicaminho",
"mainPath": "Caminho Principal",
"march7Path": "Caminho de 7 de Março",
"challenge": "Desafio",
"skipNode": "Pular Nó",
"disableSkip": "Desativar pulo",
"skipNode1": "Pular nó 1",
"skipNode2": "Pular nó 2",
"extraFeatures": "Recursos Extras",
"detailTheoryCraft": "Ativar isso permite personalizar os ciclos e ajustar o HP dos inimigos.",
"detailSkipNode": "Permite pular (Nó 1/Nó 2) em Memória do Caos ou Pura Ficção.",
"detailChallengePeak": "Permite mudar a temporada de Peak na Anomalia atual.",
"detailHiddenUi": "Irá esconder a interface do jogo.",
"detailDisableCensorship": "Desativa a censura no jogo.",
"detailMultipathCharacter": "Permite mudar o Caminho de certos personagens.",
"trailblazer": "Desbravador",
"listExtraEffect": "Lista de Efeitos Extras",
"extra": "Extra",
"customLineup": "Formação Personalizada"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Инструменты Firefly от Firefly Shelter"
},
"DataPage": {
"skillType": "Тип навыка",
"skillName": "Название навыка",
"character": "Персонаж",
"id": "ID",
"path": "Путь",
"rarity": "Редкость",
"element": "Элемент",
"technique": "Техника",
"talent": "Талант",
"basic": "Базовая атака",
"skill": "Навык",
"ultimate": "Сверхспособность",
"servant": "Слуга",
"damage": "Урон",
"type": "Тип",
"warrior": "Разрушение",
"knight": "Сохранение",
"mage": "Эрудиция",
"priest": "Изобилие",
"rogue": "Охота",
"shaman": "Гармония",
"warlock": "Небытие",
"memory": "Память",
"elation": "Радость",
"fire": "Огонь",
"ice": "Лед",
"imaginary": "Мнимый",
"physical": "Физический",
"quantum": "Квантовый",
"thunder": "Электрический",
"wind": "Ветряной",
"hp": "HP",
"atk": "Сила атаки",
"speed": "Скорость",
"critRate": "Крит. шанс",
"critDmg": "Крит. урон",
"breakEffect": "Эффект пробития",
"effectRes": "Сопротивление эффектам",
"energyRegenerationRate": "Скорость восст. энергии",
"effectHitRate": "Шанс попадания эффектов",
"outgoingHealingBoost": "Бонус исходящего исцеления",
"fireDmgBoost": "Бонус огненного урона",
"iceDmgBoost": "Бонус ледяного урона",
"imaginaryDmgBoost": "Бонус мнимого урона",
"physicalDmgBoost": "Бонус физ. урона",
"quantumDmgBoost": "Бонус квантового урона",
"thunderDmgBoost": "Бонус электр. урона",
"windDmgBoost": "Бонус ветряного урона",
"pursued": "Дополнительный урон",
"true damage": "Истинный урон",
"elationdamage": "Урон радости",
"follow-up": "Урон бонус-атаки",
"elemental damage": "Урон пробития и суперпробития",
"dot": "Периодический урон",
"qte": "QTE Навык",
"level": "Уровень",
"relics": "Реликвии",
"eidolons": "Эйдолоны",
"lightcones": "Световые конусы",
"loadData": "Загрузить данные",
"exportData": "Экспортировать данные",
"connectSetting": "Настройки подключения",
"connected": "Подключено",
"unconnected": "Не подключено",
"psConnection": "Подключение к PS",
"connectionType": "Тип подключения",
"status": "Статус",
"connectPs": "Подключить PS",
"disconnect": "Отключить",
"other": "Другое",
"freeSr": "FreeSR",
"database": "База данных",
"enka": "Enka",
"monsterSetting": "Настройки монстров",
"serverUrl": "URL сервера",
"privateType": "Приватный тип",
"local": "Локальный",
"server": "Сервер",
"username": "Имя пользователя",
"password": "Пароль",
"placeholderServerUrl": "Введите URL сервера",
"placeholderUsername": "Введите имя пользователя",
"placeholderPassword": "Введите пароль",
"connectedSuccess": "Успешное подключение к PS",
"connectedFailed": "Ошибка подключения к PS",
"syncSuccess": "Данные успешно синхронизированы с PS",
"syncFailed": "Ошибка синхронизации данных",
"sync": "Синхронизация",
"importSetting": "Настройки импорта",
"profile": "Профиль",
"default": "По умолчанию",
"copyProfiles": "Копировать профили",
"addNewProfile": "Добавить новый профиль",
"createNewProfile": "Создать новый профиль",
"editProfile": "Редактировать профиль",
"placeholderProfileName": "Введите имя профиля",
"profileName": "Имя профиля",
"create": "Создать",
"update": "Обновить",
"characterInformation": "Информация о персонаже",
"skills": "Навыки",
"showcaseCard": "Карточка персонажа",
"comingSoon": "Скоро",
"characterName": "Имя персонажа",
"placeholderCharacter": "Введите имя персонажа",
"characterSettings": "Настройки персонажа",
"levelConfiguration": "Конфигурация уровня",
"characterLevel": "Уровень персонажа",
"max": "МАКС",
"ultimateEnergy": "Энергия сверхспособности",
"currentEnergy": "Текущая энергия",
"setTo50": "Установить на 50%",
"battleConfiguration": "Конфигурация боя",
"useTechnique": "Использовать технику",
"techniqueNote": "Включить эффекты техники до боя",
"enhancement": "Усиление",
"enhancementLevel": "Уровень усиления",
"origin": "Оригинал",
"enhancedNote": "Высокие уровни открывают новые способности",
"lightconeEquipment": "Снаряжение конуса",
"lightconeSettings": "Настройки светового конуса",
"placeholderLevel": "Введите уровень",
"superimpositionRank": "Уровень наложения",
"ranksNote": "Высокие ранги дают более сильные эффекты",
"changeLightcone": "Изменить световой конус",
"removeLightcone": "Снять световой конус",
"equipLightcone": "Экипировать конус",
"noLightconeEquipped": "Световой конус не экипирован",
"equipLightconeNote": "Наденьте конус для усиления персонажа",
"filter": "Фильтр",
"selectedCharacters": "Выбранные персонажи",
"selectedProfiles": "Выбранные профили",
"clearAll": "Очистить всё",
"selectAll": "Выбрать всё",
"copy": "Копировать",
"copied": "Скопировано",
"noAvatarSelected": "Персонаж не выбран",
"noAvatarToCopySelected": "Нет персонажа для копирования",
"pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль",
"pleaseEnterUid": "Пожалуйста, введите UID",
"failedToFetchEnkaData": "Не удалось получить данные Enka",
"pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа",
"noDataToImport": "Нет данных для импорта",
"pleaseSelectAFile": "Пожалуйста, выберите файл",
"fileMustBeAValidJsonFile": "Файл должен быть в формате JSON",
"importEnkaDataSuccess": "Успешный импорт из Enka",
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
"importDatabaseSuccess": "Успешный импорт базы данных",
"getData": "Получить данные",
"import": "Импорт",
"freeSRImport": "Импорт FreeSR",
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
"pickAFile": "Выберите файл",
"lightConeSetting": "Настройки конуса",
"relicMaker": "Создатель реликвий",
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
"relicSavedSuccessfully": "Реликвия успешно сохранена",
"mainSettings": "Основные настройки",
"mainStat": "Основная характеристика",
"set": "Набор",
"pleaseSelectASet": "Выберите набор",
"effectBonus": "Бонус эффекта",
"totalRoll": "Всего улучшений",
"randomizeStats": "Случайные статы",
"randomizeRolls": "Случайные улучшения",
"selectASubStat": "Выберите доп. стат",
"selectASet": "Выберите набор",
"selectAMainStat": "Выберите основной стат",
"save": "Сохранить",
"reset": "Сброс",
"roll": "Улучшение",
"step": "Шаг",
"memoryOfChaos": "Зал забвения",
"pureFiction": "Чистый вымысел",
"apocalypticShadow": "Апокалиптическая тень",
"customEnemy": "Свой противник",
"simulatedUniverse": "Виртуальная вселенная",
"floor": "Этаж",
"side": "Половина",
"wave": "Волна",
"stage": "Стадия",
"useCycleCount": "Использовать счетчик циклов?",
"useTurbulenceBuff": "Использовать бафф турбулентности?",
"firstHalfEnemies": "Враги первой половины",
"secondHalfEnemies": "Враги второй половины",
"firstNodeEnemies": "Враги узла 1",
"secondNodeEnemies": "Враги узла 2",
"thirdNodeEnemies": "Враги узла 3",
"firstNode": "Узел 1",
"secondNode": "Узел 2",
"thirdNode": "Узел 3",
"listEnemies": "Список врагов",
"turbulenceBuff": "Бафф турбулентности",
"noEventSelected": "Событие не выбрано",
"noTurbulenceBuff": "Нет баффа турбулентности",
"upper": "Верх",
"lower": "Низ",
"upperToLower": "Верх -> Низ",
"lowerToUpper": "Низ -> Верх",
"selectMOCEvent": "Выбрать событие MOC",
"selectPFEvent": "Выбрать событие PF",
"selectASEvent": "Выбрать событие AS",
"selectCEEvent": "Выбрать событие CE",
"selectEvent": "Выбрать событие",
"selectFloor": "Выбрать этаж",
"selectSide": "Выбрать половину",
"selectBuff": "Выбрать бафф",
"selectStage": "Выбрать стадию",
"previous": "Назад",
"next": "Вперед",
"noMonstersFound": "Монстры не найдены",
"addNewWave": "Добавить новую волну",
"searchStage": "Поиск стадии...",
"noStageFound": "Стадия не найдена",
"searchMonster": "Поиск монстра...",
"changeRelic": "Изменить реликвию",
"deleteRelic": "Удалить реликвию",
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
"setEffects": "Настроить эффекты",
"details": "Детали",
"normal": "Базовая атака",
"bpskill": "Навык",
"maze": "Техника",
"ultra": "Сверхспособность",
"servantskill": "Навык мемоспрайта",
"severaltalent": "Талант мемоспрайта",
"singleattack": "Одиночная атака",
"enhance": "Усиление",
"summon": "Призыв",
"mazeattack": "Атака в технике",
"blast": "Взрыв",
"restore": "Восстановление",
"support": "Поддержка",
"aoeattack": "АоЕ атака",
"impair": "Ослабление",
"bounce": "Отскок",
"active": "Активен",
"defence": "Защита",
"inactive": "Неактивен",
"maxAll": "Улучшить всё",
"maxAllSuccess": "Уровни навыков улучшены до максимума.",
"maxAllFailed": "Не удалось улучшить навыки.",
"noRelicEquipped": "Нет экипированных реликвий",
"anomalyArbitration": "Аномальный арбитраж",
"normalMode": "Обычный режим",
"hardMode": "Сложный режим",
"selectPEAKEvent": "Выбрать событие PEAK",
"mode": "Режим",
"selectMode": "Выбрать режим",
"rollBack": "Отменить шаг",
"upRoll": "Повысить улучшение",
"downRoll": "Понизить улучшение",
"actions": "Действия",
"avatars": "Персонажи",
"quickView": "Быстрый просмотр",
"extraSetting": "Дополнительные настройки",
"disableCensorship": "Отключить цензуру",
"hideUI": "Скрыть интерфейс",
"theoryCraftMode": "Режим Theorycraft",
"cycleCount": "Счетчик циклов",
"pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы",
"subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Многопутевой персонаж",
"mainPath": "Основной Путь",
"march7Path": "Путь Март 7",
"challenge": "Испытание",
"skipNode": "Пропустить узел",
"disableSkip": "Отключить пропуск",
"skipNode1": "Пропустить узел 1",
"skipNode2": "Пропустить узел 2",
"extraFeatures": "Дополнительные функции",
"detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.",
"detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.",
"detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.",
"detailHiddenUi": "Скрывает игровой интерфейс.",
"detailDisableCensorship": "Отключает внутриигровую цензуру.",
"detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.",
"trailblazer": "Первопроходец",
"listExtraEffect": "Список доп. эффектов",
"extra": "Экстра",
"customLineup": "Пользовательский отряд"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Инструменты Firefly от Firefly Shelter"
},
"DataPage": {
"skillType": "Тип навыка",
"skillName": "Название навыка",
"character": "Персонаж",
"id": "ID",
"path": "Путь",
"rarity": "Редкость",
"element": "Элемент",
"technique": "Техника",
"talent": "Талант",
"basic": "Базовая атака",
"skill": "Навык",
"ultimate": "Сверхспособность",
"servant": "Слуга",
"damage": "Урон",
"type": "Тип",
"warrior": "Разрушение",
"knight": "Сохранение",
"mage": "Эрудиция",
"priest": "Изобилие",
"rogue": "Охота",
"shaman": "Гармония",
"warlock": "Небытие",
"memory": "Память",
"elation": "Радость",
"fire": "Огонь",
"ice": "Лед",
"imaginary": "Мнимый",
"physical": "Физический",
"quantum": "Квантовый",
"thunder": "Электрический",
"wind": "Ветряной",
"hp": "HP",
"atk": "Сила атаки",
"speed": "Скорость",
"critRate": "Крит. шанс",
"critDmg": "Крит. урон",
"breakEffect": "Эффект пробития",
"effectRes": "Сопротивление эффектам",
"energyRegenerationRate": "Скорость восст. энергии",
"effectHitRate": "Шанс попадания эффектов",
"outgoingHealingBoost": "Бонус исходящего исцеления",
"fireDmgBoost": "Бонус огненного урона",
"iceDmgBoost": "Бонус ледяного урона",
"imaginaryDmgBoost": "Бонус мнимого урона",
"physicalDmgBoost": "Бонус физ. урона",
"quantumDmgBoost": "Бонус квантового урона",
"thunderDmgBoost": "Бонус электр. урона",
"windDmgBoost": "Бонус ветряного урона",
"pursued": "Дополнительный урон",
"true damage": "Истинный урон",
"elationdamage": "Урон радости",
"follow-up": "Урон бонус-атаки",
"elemental damage": "Урон пробития и суперпробития",
"dot": "Периодический урон",
"qte": "QTE Навык",
"level": "Уровень",
"relics": "Реликвии",
"eidolons": "Эйдолоны",
"lightcones": "Световые конусы",
"loadData": "Загрузить данные",
"exportData": "Экспортировать данные",
"connectSetting": "Настройки подключения",
"connected": "Подключено",
"unconnected": "Не подключено",
"psConnection": "Подключение к PS",
"connectionType": "Тип подключения",
"status": "Статус",
"connectPs": "Подключить PS",
"disconnect": "Отключить",
"other": "Другое",
"freeSr": "FreeSR",
"database": "База данных",
"enka": "Enka",
"monsterSetting": "Настройки монстров",
"serverUrl": "URL сервера",
"privateType": "Приватный тип",
"local": "Локальный",
"server": "Сервер",
"username": "Имя пользователя",
"password": "Пароль",
"placeholderServerUrl": "Введите URL сервера",
"placeholderUsername": "Введите имя пользователя",
"placeholderPassword": "Введите пароль",
"connectedSuccess": "Успешное подключение к PS",
"connectedFailed": "Ошибка подключения к PS",
"syncSuccess": "Данные успешно синхронизированы с PS",
"syncFailed": "Ошибка синхронизации данных",
"sync": "Синхронизация",
"importSetting": "Настройки импорта",
"profile": "Профиль",
"default": "По умолчанию",
"copyProfiles": "Копировать профили",
"addNewProfile": "Добавить новый профиль",
"createNewProfile": "Создать новый профиль",
"editProfile": "Редактировать профиль",
"placeholderProfileName": "Введите имя профиля",
"profileName": "Имя профиля",
"create": "Создать",
"update": "Обновить",
"characterInformation": "Информация о персонаже",
"skills": "Навыки",
"showcaseCard": "Карточка персонажа",
"comingSoon": "Скоро",
"characterName": "Имя персонажа",
"placeholderCharacter": "Введите имя персонажа",
"characterSettings": "Настройки персонажа",
"levelConfiguration": "Конфигурация уровня",
"characterLevel": "Уровень персонажа",
"max": "МАКС",
"ultimateEnergy": "Энергия сверхспособности",
"currentEnergy": "Текущая энергия",
"setTo50": "Установить на 50%",
"battleConfiguration": "Конфигурация боя",
"useTechnique": "Использовать технику",
"techniqueNote": "Включить эффекты техники до боя",
"enhancement": "Усиление",
"enhancementLevel": "Уровень усиления",
"origin": "Оригинал",
"enhancedNote": "Высокие уровни открывают новые способности",
"lightconeEquipment": "Снаряжение конуса",
"lightconeSettings": "Настройки светового конуса",
"placeholderLevel": "Введите уровень",
"superimpositionRank": "Уровень наложения",
"ranksNote": "Высокие ранги дают более сильные эффекты",
"changeLightcone": "Изменить световой конус",
"removeLightcone": "Снять световой конус",
"equipLightcone": "Экипировать конус",
"noLightconeEquipped": "Световой конус не экипирован",
"equipLightconeNote": "Наденьте конус для усиления персонажа",
"filter": "Фильтр",
"selectedCharacters": "Выбранные персонажи",
"selectedProfiles": "Выбранные профили",
"clearAll": "Очистить всё",
"selectAll": "Выбрать всё",
"copy": "Копировать",
"copied": "Скопировано",
"noAvatarSelected": "Персонаж не выбран",
"noAvatarToCopySelected": "Нет персонажа для копирования",
"pleaseSelectAtLeastOneProfile": "Пожалуйста, выберите хотя бы один профиль",
"pleaseEnterUid": "Пожалуйста, введите UID",
"failedToFetchEnkaData": "Не удалось получить данные Enka",
"pleaseSelectAtLeastOneCharacter": "Пожалуйста, выберите хотя бы одного персонажа",
"noDataToImport": "Нет данных для импорта",
"pleaseSelectAFile": "Пожалуйста, выберите файл",
"fileMustBeAValidJsonFile": "Файл должен быть в формате JSON",
"importEnkaDataSuccess": "Успешный импорт из Enka",
"importFreeSRDataSuccess": "Успешный импорт из FreeSR",
"importDatabaseSuccess": "Успешный импорт базы данных",
"getData": "Получить данные",
"import": "Импорт",
"freeSRImport": "Импорт FreeSR",
"onlySupportFreeSRJsonFile": "Поддерживаются только JSON от FreeSR",
"pickAFile": "Выберите файл",
"lightConeSetting": "Настройки конуса",
"relicMaker": "Создатель реликвий",
"pleaseSelectAllOptions": "Пожалуйста, выберите все параметры",
"relicSavedSuccessfully": "Реликвия успешно сохранена",
"mainSettings": "Основные настройки",
"mainStat": "Основная характеристика",
"set": "Набор",
"pleaseSelectASet": "Выберите набор",
"effectBonus": "Бонус эффекта",
"totalRoll": "Всего улучшений",
"randomizeStats": "Случайные статы",
"randomizeRolls": "Случайные улучшения",
"selectASubStat": "Выберите доп. стат",
"selectASet": "Выберите набор",
"selectAMainStat": "Выберите основной стат",
"save": "Сохранить",
"reset": "Сброс",
"roll": "Улучшение",
"step": "Шаг",
"memoryOfChaos": "Зал забвения",
"pureFiction": "Чистый вымысел",
"apocalypticShadow": "Апокалиптическая тень",
"customEnemy": "Свой противник",
"simulatedUniverse": "Виртуальная вселенная",
"floor": "Этаж",
"side": "Половина",
"wave": "Волна",
"stage": "Стадия",
"useCycleCount": "Использовать счетчик циклов?",
"useTurbulenceBuff": "Использовать бафф турбулентности?",
"firstHalfEnemies": "Враги первой половины",
"secondHalfEnemies": "Враги второй половины",
"firstNodeEnemies": "Враги узла 1",
"secondNodeEnemies": "Враги узла 2",
"thirdNodeEnemies": "Враги узла 3",
"firstNode": "Узел 1",
"secondNode": "Узел 2",
"thirdNode": "Узел 3",
"listEnemies": "Список врагов",
"turbulenceBuff": "Бафф турбулентности",
"noEventSelected": "Событие не выбрано",
"noTurbulenceBuff": "Нет баффа турбулентности",
"upper": "Верх",
"lower": "Низ",
"upperToLower": "Верх -> Низ",
"lowerToUpper": "Низ -> Верх",
"selectMOCEvent": "Выбрать событие MOC",
"selectPFEvent": "Выбрать событие PF",
"selectASEvent": "Выбрать событие AS",
"selectCEEvent": "Выбрать событие CE",
"selectEvent": "Выбрать событие",
"selectFloor": "Выбрать этаж",
"selectSide": "Выбрать половину",
"selectBuff": "Выбрать бафф",
"selectStage": "Выбрать стадию",
"previous": "Назад",
"next": "Вперед",
"noMonstersFound": "Монстры не найдены",
"addNewWave": "Добавить новую волну",
"searchStage": "Поиск стадии...",
"noStageFound": "Стадия не найдена",
"searchMonster": "Поиск монстра...",
"changeRelic": "Изменить реликвию",
"deleteRelic": "Удалить реликвию",
"deleteRelicConfirm": "Удалить реликвию в этом слоте?",
"setEffects": "Настроить эффекты",
"details": "Детали",
"normal": "Базовая атака",
"bpskill": "Навык",
"maze": "Техника",
"ultra": "Сверхспособность",
"servantskill": "Навык мемоспрайта",
"severaltalent": "Талант мемоспрайта",
"singleattack": "Одиночная атака",
"enhance": "Усиление",
"summon": "Призыв",
"mazeattack": "Атака в технике",
"blast": "Взрыв",
"restore": "Восстановление",
"support": "Поддержка",
"aoeattack": "АоЕ атака",
"impair": "Ослабление",
"bounce": "Отскок",
"active": "Активен",
"defence": "Защита",
"inactive": "Неактивен",
"maxAll": "Улучшить всё",
"maxAllSuccess": "Уровни навыков улучшены до максимума.",
"maxAllFailed": "Не удалось улучшить навыки.",
"noRelicEquipped": "Нет экипированных реликвий",
"anomalyArbitration": "Аномальный арбитраж",
"normalMode": "Обычный режим",
"hardMode": "Сложный режим",
"selectPEAKEvent": "Выбрать событие PEAK",
"mode": "Режим",
"selectMode": "Выбрать режим",
"rollBack": "Отменить шаг",
"upRoll": "Повысить улучшение",
"downRoll": "Понизить улучшение",
"actions": "Действия",
"avatars": "Персонажи",
"quickView": "Быстрый просмотр",
"extraSetting": "Дополнительные настройки",
"disableCensorship": "Отключить цензуру",
"hideUI": "Скрыть интерфейс",
"theoryCraftMode": "Режим Theorycraft",
"cycleCount": "Счетчик циклов",
"pleaseSelectAllSubStats": "Пожалуйста, выберите все саб-статы",
"subStatRollCountCannotBeZero": "Количество улучшений саб-стата не может быть нулем",
"theoryCraft": "Theorycraft",
"multipathCharacter": "Многопутевой персонаж",
"mainPath": "Основной Путь",
"march7Path": "Путь Март 7",
"challenge": "Испытание",
"skipNode": "Пропустить узел",
"disableSkip": "Отключить пропуск",
"skipNode1": "Пропустить узел 1",
"skipNode2": "Пропустить узел 2",
"extraFeatures": "Дополнительные функции",
"detailTheoryCraft": "Включение этой функции позволяет настроить количество циклов и здоровье противников.",
"detailSkipNode": "Позволяет пропустить (Узел 1/Узел 2) в Зале забвения или Чистом вымысле.",
"detailChallengePeak": "Позволяет изменить сезон Peak в текущей Аномалии.",
"detailHiddenUi": "Скрывает игровой интерфейс.",
"detailDisableCensorship": "Отключает внутриигровую цензуру.",
"detailMultipathCharacter": "Позволяет изменить Путь некоторых персонажей.",
"trailblazer": "Первопроходец",
"listExtraEffect": "Список доп. эффектов",
"extra": "Экстра",
"customLineup": "Пользовательский отряд"
}
}
+289 -289
View File
@@ -1,290 +1,290 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "เครื่องมือ Firefly โดย Firefly Shelter"
},
"DataPage": {
"skillType": "ประเภทสกิล",
"skillName": "ชื่อสกิล",
"character": "ตัวละคร",
"id": "ไอดี",
"path": "Path",
"rarity": "ระดับความหายาก",
"element": "ธาตุ",
"technique": "เทคนิค",
"talent": "พรสวรรค์",
"basic": "โจมตีปกติ",
"skill": "สกิลต่อสู้",
"ultimate": "ท่าไม้ตาย",
"servant": "ผู้ติดตาม",
"damage": "ความเสียหาย",
"type": "ประเภท",
"warrior": "ทำลายล้าง",
"knight": "อนุรักษ์",
"mage": "ปัญญา",
"priest": "เฟื่องฟู",
"rogue": "ล่าสังหาร",
"shaman": "ประสาน",
"warlock": "ลบล้าง",
"memory": "ลบล้าง",
"elation": "ปิติสุข",
"fire": "ไฟ",
"ice": "น้ำแข็ง",
"imaginary": "จินตภาพ",
"physical": "กายภาพ",
"quantum": "ควอนตัม",
"thunder": "สายฟ้า",
"wind": "ลม",
"hp": "HP",
"atk": "ATK",
"speed": "ความเร็ว",
"critRate": "อัตราคริติคอล",
"critDmg": "ความเสียหายคริติคอล",
"breakEffect": "เอฟเฟกต์ทำลายล้าง",
"effectRes": "ต้านทานสถานะ",
"energyRegenerationRate": "อัตราฟื้นฟูพลังงาน",
"effectHitRate": "อัตราสร้างสถานะ",
"outgoingHealingBoost": "โบนัสการรักษา",
"fireDmgBoost": "โบนัสความเสียหายไฟ",
"iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง",
"imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ",
"physicalDmgBoost": "โบนัสความเสียหายกายภาพ",
"quantumDmgBoost": "โบนัสความเสียหายควอนตัม",
"thunderDmgBoost": "โบนัสความเสียหายสายฟ้า",
"windDmgBoost": "โบนัสความเสียหายลม",
"pursued": "ความเสียหายเพิ่มเติม",
"true damage": "ความเสียหายจริง",
"elationdamage": "ความเสียหายปิติสุข",
"follow-up": "ความเสียหายโจมตีต่อเนื่อง",
"elemental damage": "ความเสียหายทำลายล้างและซูเปอร์",
"dot": "ความเสียหายต่อเนื่อง",
"qte": "สกิล QTE",
"level": "เลเวล",
"relics": "รีลิกส์",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "โหลดข้อมูล",
"exportData": "ส่งออกข้อมูล",
"connectSetting": "การตั้งค่าการเชื่อมต่อ",
"connected": "เชื่อมต่อแล้ว",
"unconnected": "ไม่ได้เชื่อมต่อ",
"psConnection": "การเชื่อมต่อ PS",
"connectionType": "ประเภทการเชื่อมต่อ",
"status": "สถานะ",
"connectPs": "เชื่อมต่อ PS",
"disconnect": "ยกเลิกการเชื่อมต่อ",
"other": "อื่นๆ",
"freeSr": "FreeSR",
"database": "ฐานข้อมูล",
"enka": "Enka",
"monsterSetting": "ตั้งค่ามอนสเตอร์",
"serverUrl": "URL เซิร์ฟเวอร์",
"privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว",
"local": "ในเครื่อง",
"server": "เซิร์ฟเวอร์",
"username": "ชื่อผู้ใช้",
"password": "รหัสผ่าน",
"placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์",
"placeholderUsername": "ป้อนชื่อผู้ใช้",
"placeholderPassword": "ป้อนรหัสผ่าน",
"connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ",
"connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS",
"syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ",
"syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS",
"sync": "ซิงค์",
"importSetting": "การตั้งค่าการนำเข้า",
"profile": "โปรไฟล์",
"default": "ค่าเริ่มต้น",
"copyProfiles": "คัดลอกโปรไฟล์",
"addNewProfile": "เพิ่มโปรไฟล์ใหม่",
"createNewProfile": "สร้างโปรไฟล์ใหม่",
"editProfile": "แก้ไขโปรไฟล์",
"placeholderProfileName": "ป้อนชื่อโปรไฟล์",
"profileName": "ชื่อโปรไฟล์",
"create": "สร้าง",
"update": "อัปเดต",
"characterInformation": "ข้อมูลตัวละคร",
"skills": "สกิล",
"showcaseCard": "การ์ดแสดง",
"comingSoon": "เร็วๆ นี้",
"characterName": "ชื่อตัวละคร",
"placeholderCharacter": "ป้อนชื่อตัวละคร",
"characterSettings": "การตั้งค่าตัวละคร",
"levelConfiguration": "การกำหนดระดับเลเวล",
"characterLevel": "เลเวลตัวละคร",
"max": "สูงสุด",
"ultimateEnergy": "พลังงานท่าไม้ตาย",
"currentEnergy": "พลังงานปัจจุบัน",
"setTo50": "ตั้งค่าเป็น 50%",
"battleConfiguration": "การกำหนดค่าการต่อสู้",
"useTechnique": "ใช้เทคนิค",
"techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้",
"enhancement": "การเสริมพลัง",
"enhancementLevel": "ระดับการเสริมพลัง",
"origin": "ดั้งเดิม",
"enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ",
"lightconeEquipment": "สวมใส่ Light Cone",
"lightconeSettings": "การตั้งค่า Light Cone",
"placeholderLevel": "ป้อนเลเวล",
"superimpositionRank": "ระดับการขัดเกลา",
"ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า",
"changeLightcone": "เปลี่ยน Light Cone",
"removeLightcone": "ถอด Light Cone",
"equipLightcone": "สวมใส่ Light Cone",
"noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone",
"equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง",
"filter": "กรอง",
"selectedCharacters": "ตัวละครที่เลือก",
"selectedProfiles": "โปรไฟล์ที่เลือก",
"clearAll": "ล้างทั้งหมด",
"selectAll": "เลือกทั้งหมด",
"copy": "คัดลอก",
"copied": "คัดลอกแล้ว",
"noAvatarSelected": "ไม่ได้เลือกตัวละคร",
"noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก",
"pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์",
"pleaseEnterUid": "โปรดป้อน UID",
"failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้",
"pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร",
"noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า",
"pleaseSelectAFile": "โปรดเลือกไฟล์",
"fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง",
"importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ",
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
"getData": "รับข้อมูล",
"import": "นำเข้า",
"freeSRImport": "นำเข้า FreeSR",
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
"pickAFile": "เลือกไฟล์",
"lightConeSetting": "การตั้งค่า Light Cone",
"relicMaker": "สร้างรีลิกส์",
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
"relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ",
"mainSettings": "การตั้งค่าหลัก",
"mainStat": "ค่าสเตตัสหลัก",
"set": "เซ็ต",
"pleaseSelectASet": "โปรดเลือกเซ็ต",
"effectBonus": "โบนัสเอฟเฟกต์",
"totalRoll": "จำนวนการอัปเกรด",
"randomizeStats": "สุ่มสเตตัส",
"randomizeRolls": "สุ่มอัปเกรด",
"selectASubStat": "เลือกสเตตัสรอง",
"selectASet": "เลือกเซ็ต",
"selectAMainStat": "เลือกสเตตัสหลัก",
"save": "บันทึก",
"reset": "รีเซ็ต",
"roll": "อัปเกรด",
"step": "ขั้น",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "ศัตรูกำหนดเอง",
"simulatedUniverse": "Simulated Universe",
"floor": "ชั้น",
"side": "ฝั่ง",
"wave": "เวฟ",
"stage": "ด่าน",
"useCycleCount": "ใช้การนับรอบหรือไม่?",
"useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?",
"firstHalfEnemies": "ศัตรูครึ่งแรก",
"secondHalfEnemies": "ศัตรูครึ่งหลัง",
"firstNodeEnemies": "ศัตรูโหนด 1",
"secondNodeEnemies": "ศัตรูโหนด 2",
"thirdNodeEnemies": "ศัตรูโหนด 3",
"firstNode": "โหนด 1",
"secondNode": "โหนด 2",
"thirdNode": "โหนด 3",
"listEnemies": "รายการศัตรู",
"turbulenceBuff": "บัฟบรรยากาศ",
"noEventSelected": "ไม่ได้เลือกอีเวนต์",
"noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ",
"upper": "บน",
"lower": "ล่าง",
"upperToLower": "บน -> ล่าง",
"lowerToUpper": "ล่าง -> บน",
"selectMOCEvent": "เลือกอีเวนต์ MOC",
"selectPFEvent": "เลือกอีเวนต์ PF",
"selectASEvent": "เลือกอีเวนต์ AS",
"selectCEEvent": "เลือกอีเวนต์ CE",
"selectEvent": "เลือกอีเวนต์",
"selectFloor": "เลือกชั้น",
"selectSide": "เลือกฝั่ง",
"selectBuff": "เลือกบัฟ",
"selectStage": "เลือกด่าน",
"previous": "ก่อนหน้า",
"next": "ถัดไป",
"noMonstersFound": "ไม่พบมอนสเตอร์",
"addNewWave": "เพิ่มเวฟใหม่",
"searchStage": "ค้นหาด่าน...",
"noStageFound": "ไม่พบด่าน",
"searchMonster": "ค้นหามอนสเตอร์...",
"changeRelic": "เปลี่ยนรีลิกส์",
"deleteRelic": "ลบรีลิกส์",
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
"setEffects": "ตั้งค่าเอฟเฟกต์",
"details": "รายละเอียด",
"normal": "โจมตีปกติ",
"bpskill": "สกิลต่อสู้",
"maze": "เทคนิค",
"ultra": "ท่าไม้ตาย",
"servantskill": "สกิล Memosprite",
"severaltalent": "พรสวรรค์ Memosprite",
"singleattack": "โจมตีเป้าหมายเดียว",
"enhance": "เสริมพลัง",
"summon": "อัญเชิญ",
"mazeattack": "โจมตีแบบเทคนิค",
"blast": "กระจาย",
"restore": "ฟื้นฟู",
"support": "สนับสนุน",
"aoeattack": "โจมตีหมู่",
"impair": "ป่วน",
"bounce": "ชิ่ง",
"active": "เปิดใช้งาน",
"defence": "ป้องกัน",
"inactive": "ปิดใช้งาน",
"maxAll": "อัปเกรดทั้งหมด",
"maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ",
"maxAllFailed": "อัปเกรดสกิลล้มเหลว",
"noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "โหมดปกติ",
"hardMode": "โหมดยาก",
"selectPEAKEvent": "เลือกอีเวนต์ PEAK",
"mode": "โหมด",
"selectMode": "เลือกโหมด",
"rollBack": "ย้อนกลับ",
"upRoll": "เพิ่มอัปเกรด",
"downRoll": "ลดอัปเกรด",
"actions": "แอ็กชัน",
"avatars": "อวตาร",
"quickView": "ดูแบบด่วน",
"extraSetting": "การตั้งค่าเพิ่มเติม",
"disableCensorship": "ปิดการเซ็นเซอร์",
"hideUI": "ซ่อน UI",
"theoryCraftMode": "โหมด Theorycraft",
"cycleCount": "จำนวนรอบ",
"pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด",
"subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์",
"theoryCraft": "Theorycraft",
"multipathCharacter": "ตัวละครหลาย Path",
"mainPath": "Path หลัก",
"march7Path": "Path March 7th",
"challenge": "ท้าทาย",
"skipNode": "ข้ามโหนด",
"disableSkip": "ปิดใช้งานการข้าม",
"skipNode1": "ข้ามโหนด 1",
"skipNode2": "ข้ามโหนด 2",
"extraFeatures": "คุณสมบัติพิเศษ",
"detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้",
"detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction",
"detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน",
"detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม",
"detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม",
"detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว",
"trailblazer": "ผู้บุกเบิก",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"extra": "พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "เครื่องมือ Firefly โดย Firefly Shelter"
},
"DataPage": {
"skillType": "ประเภทสกิล",
"skillName": "ชื่อสกิล",
"character": "ตัวละคร",
"id": "ไอดี",
"path": "Path",
"rarity": "ระดับความหายาก",
"element": "ธาตุ",
"technique": "เทคนิค",
"talent": "พรสวรรค์",
"basic": "โจมตีปกติ",
"skill": "สกิลต่อสู้",
"ultimate": "ท่าไม้ตาย",
"servant": "ผู้ติดตาม",
"damage": "ความเสียหาย",
"type": "ประเภท",
"warrior": "ทำลายล้าง",
"knight": "อนุรักษ์",
"mage": "ปัญญา",
"priest": "เฟื่องฟู",
"rogue": "ล่าสังหาร",
"shaman": "ประสาน",
"warlock": "ลบล้าง",
"memory": "ลบล้าง",
"elation": "ปิติสุข",
"fire": "ไฟ",
"ice": "น้ำแข็ง",
"imaginary": "จินตภาพ",
"physical": "กายภาพ",
"quantum": "ควอนตัม",
"thunder": "สายฟ้า",
"wind": "ลม",
"hp": "HP",
"atk": "ATK",
"speed": "ความเร็ว",
"critRate": "อัตราคริติคอล",
"critDmg": "ความเสียหายคริติคอล",
"breakEffect": "เอฟเฟกต์ทำลายล้าง",
"effectRes": "ต้านทานสถานะ",
"energyRegenerationRate": "อัตราฟื้นฟูพลังงาน",
"effectHitRate": "อัตราสร้างสถานะ",
"outgoingHealingBoost": "โบนัสการรักษา",
"fireDmgBoost": "โบนัสความเสียหายไฟ",
"iceDmgBoost": "โบนัสความเสียหายน้ำแข็ง",
"imaginaryDmgBoost": "โบนัสความเสียหายจินตภาพ",
"physicalDmgBoost": "โบนัสความเสียหายกายภาพ",
"quantumDmgBoost": "โบนัสความเสียหายควอนตัม",
"thunderDmgBoost": "โบนัสความเสียหายสายฟ้า",
"windDmgBoost": "โบนัสความเสียหายลม",
"pursued": "ความเสียหายเพิ่มเติม",
"true damage": "ความเสียหายจริง",
"elationdamage": "ความเสียหายปิติสุข",
"follow-up": "ความเสียหายโจมตีต่อเนื่อง",
"elemental damage": "ความเสียหายทำลายล้างและซูเปอร์",
"dot": "ความเสียหายต่อเนื่อง",
"qte": "สกิล QTE",
"level": "เลเวล",
"relics": "รีลิกส์",
"eidolons": "Eidolon",
"lightcones": "Light Cone",
"loadData": "โหลดข้อมูล",
"exportData": "ส่งออกข้อมูล",
"connectSetting": "การตั้งค่าการเชื่อมต่อ",
"connected": "เชื่อมต่อแล้ว",
"unconnected": "ไม่ได้เชื่อมต่อ",
"psConnection": "การเชื่อมต่อ PS",
"connectionType": "ประเภทการเชื่อมต่อ",
"status": "สถานะ",
"connectPs": "เชื่อมต่อ PS",
"disconnect": "ยกเลิกการเชื่อมต่อ",
"other": "อื่นๆ",
"freeSr": "FreeSR",
"database": "ฐานข้อมูล",
"enka": "Enka",
"monsterSetting": "ตั้งค่ามอนสเตอร์",
"serverUrl": "URL เซิร์ฟเวอร์",
"privateType": "ประเภทเซิร์ฟเวอร์ส่วนตัว",
"local": "ในเครื่อง",
"server": "เซิร์ฟเวอร์",
"username": "ชื่อผู้ใช้",
"password": "รหัสผ่าน",
"placeholderServerUrl": "ป้อน URL เซิร์ฟเวอร์",
"placeholderUsername": "ป้อนชื่อผู้ใช้",
"placeholderPassword": "ป้อนรหัสผ่าน",
"connectedSuccess": "เชื่อมต่อกับ PS สำเร็จ",
"connectedFailed": "ล้มเหลวในการเชื่อมต่อกับ PS",
"syncSuccess": "ซิงค์ข้อมูลกับ PS สำเร็จ",
"syncFailed": "ล้มเหลวในการซิงค์ข้อมูลกับ PS",
"sync": "ซิงค์",
"importSetting": "การตั้งค่าการนำเข้า",
"profile": "โปรไฟล์",
"default": "ค่าเริ่มต้น",
"copyProfiles": "คัดลอกโปรไฟล์",
"addNewProfile": "เพิ่มโปรไฟล์ใหม่",
"createNewProfile": "สร้างโปรไฟล์ใหม่",
"editProfile": "แก้ไขโปรไฟล์",
"placeholderProfileName": "ป้อนชื่อโปรไฟล์",
"profileName": "ชื่อโปรไฟล์",
"create": "สร้าง",
"update": "อัปเดต",
"characterInformation": "ข้อมูลตัวละคร",
"skills": "สกิล",
"showcaseCard": "การ์ดแสดง",
"comingSoon": "เร็วๆ นี้",
"characterName": "ชื่อตัวละคร",
"placeholderCharacter": "ป้อนชื่อตัวละคร",
"characterSettings": "การตั้งค่าตัวละคร",
"levelConfiguration": "การกำหนดระดับเลเวล",
"characterLevel": "เลเวลตัวละคร",
"max": "สูงสุด",
"ultimateEnergy": "พลังงานท่าไม้ตาย",
"currentEnergy": "พลังงานปัจจุบัน",
"setTo50": "ตั้งค่าเป็น 50%",
"battleConfiguration": "การกำหนดค่าการต่อสู้",
"useTechnique": "ใช้เทคนิค",
"techniqueNote": "เปิดใช้เอฟเฟกต์เทคนิคก่อนการต่อสู้",
"enhancement": "การเสริมพลัง",
"enhancementLevel": "ระดับการเสริมพลัง",
"origin": "ดั้งเดิม",
"enhancedNote": "การเสริมพลังที่สูงขึ้นจะปลดล็อกความสามารถ",
"lightconeEquipment": "สวมใส่ Light Cone",
"lightconeSettings": "การตั้งค่า Light Cone",
"placeholderLevel": "ป้อนเลเวล",
"superimpositionRank": "ระดับการขัดเกลา",
"ranksNote": "ระดับที่สูงขึ้นให้เอฟเฟกต์ที่แข็งแกร่งกว่า",
"changeLightcone": "เปลี่ยน Light Cone",
"removeLightcone": "ถอด Light Cone",
"equipLightcone": "สวมใส่ Light Cone",
"noLightconeEquipped": "ไม่ได้สวมใส่ Light Cone",
"equipLightconeNote": "สวมใส่ Light Cone เพื่อเพิ่มพลัง",
"filter": "กรอง",
"selectedCharacters": "ตัวละครที่เลือก",
"selectedProfiles": "โปรไฟล์ที่เลือก",
"clearAll": "ล้างทั้งหมด",
"selectAll": "เลือกทั้งหมด",
"copy": "คัดลอก",
"copied": "คัดลอกแล้ว",
"noAvatarSelected": "ไม่ได้เลือกตัวละคร",
"noAvatarToCopySelected": "ไม่ได้เลือกตัวละครที่จะคัดลอก",
"pleaseSelectAtLeastOneProfile": "โปรดเลือกอย่างน้อยหนึ่งโปรไฟล์",
"pleaseEnterUid": "โปรดป้อน UID",
"failedToFetchEnkaData": "ไม่สามารถดึงข้อมูล Enka ได้",
"pleaseSelectAtLeastOneCharacter": "โปรดเลือกอย่างน้อยหนึ่งตัวละคร",
"noDataToImport": "ไม่มีข้อมูลที่จะนำเข้า",
"pleaseSelectAFile": "โปรดเลือกไฟล์",
"fileMustBeAValidJsonFile": "ไฟล์ต้องเป็นไฟล์ JSON ที่ถูกต้อง",
"importEnkaDataSuccess": "นำเข้าข้อมูล Enka สำเร็จ",
"importFreeSRDataSuccess": "นำเข้าข้อมูล FreeSR สำเร็จ",
"importDatabaseSuccess": "นำเข้าฐานข้อมูลสำเร็จ",
"getData": "รับข้อมูล",
"import": "นำเข้า",
"freeSRImport": "นำเข้า FreeSR",
"onlySupportFreeSRJsonFile": "รองรับเฉพาะไฟล์ JSON ของ FreeSR",
"pickAFile": "เลือกไฟล์",
"lightConeSetting": "การตั้งค่า Light Cone",
"relicMaker": "สร้างรีลิกส์",
"pleaseSelectAllOptions": "โปรดเลือกตัวเลือกทั้งหมด",
"relicSavedSuccessfully": "บันทึกรีลิกส์สำเร็จ",
"mainSettings": "การตั้งค่าหลัก",
"mainStat": "ค่าสเตตัสหลัก",
"set": "เซ็ต",
"pleaseSelectASet": "โปรดเลือกเซ็ต",
"effectBonus": "โบนัสเอฟเฟกต์",
"totalRoll": "จำนวนการอัปเกรด",
"randomizeStats": "สุ่มสเตตัส",
"randomizeRolls": "สุ่มอัปเกรด",
"selectASubStat": "เลือกสเตตัสรอง",
"selectASet": "เลือกเซ็ต",
"selectAMainStat": "เลือกสเตตัสหลัก",
"save": "บันทึก",
"reset": "รีเซ็ต",
"roll": "อัปเกรด",
"step": "ขั้น",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "ศัตรูกำหนดเอง",
"simulatedUniverse": "Simulated Universe",
"floor": "ชั้น",
"side": "ฝั่ง",
"wave": "เวฟ",
"stage": "ด่าน",
"useCycleCount": "ใช้การนับรอบหรือไม่?",
"useTurbulenceBuff": "ใช้บัฟบรรยากาศหรือไม่?",
"firstHalfEnemies": "ศัตรูครึ่งแรก",
"secondHalfEnemies": "ศัตรูครึ่งหลัง",
"firstNodeEnemies": "ศัตรูโหนด 1",
"secondNodeEnemies": "ศัตรูโหนด 2",
"thirdNodeEnemies": "ศัตรูโหนด 3",
"firstNode": "โหนด 1",
"secondNode": "โหนด 2",
"thirdNode": "โหนด 3",
"listEnemies": "รายการศัตรู",
"turbulenceBuff": "บัฟบรรยากาศ",
"noEventSelected": "ไม่ได้เลือกอีเวนต์",
"noTurbulenceBuff": "ไม่มีบัฟบรรยากาศ",
"upper": "บน",
"lower": "ล่าง",
"upperToLower": "บน -> ล่าง",
"lowerToUpper": "ล่าง -> บน",
"selectMOCEvent": "เลือกอีเวนต์ MOC",
"selectPFEvent": "เลือกอีเวนต์ PF",
"selectASEvent": "เลือกอีเวนต์ AS",
"selectCEEvent": "เลือกอีเวนต์ CE",
"selectEvent": "เลือกอีเวนต์",
"selectFloor": "เลือกชั้น",
"selectSide": "เลือกฝั่ง",
"selectBuff": "เลือกบัฟ",
"selectStage": "เลือกด่าน",
"previous": "ก่อนหน้า",
"next": "ถัดไป",
"noMonstersFound": "ไม่พบมอนสเตอร์",
"addNewWave": "เพิ่มเวฟใหม่",
"searchStage": "ค้นหาด่าน...",
"noStageFound": "ไม่พบด่าน",
"searchMonster": "ค้นหามอนสเตอร์...",
"changeRelic": "เปลี่ยนรีลิกส์",
"deleteRelic": "ลบรีลิกส์",
"deleteRelicConfirm": "คุณแน่ใจหรือไม่ว่าต้องการลบรีลิกส์ในช่องนี้?",
"setEffects": "ตั้งค่าเอฟเฟกต์",
"details": "รายละเอียด",
"normal": "โจมตีปกติ",
"bpskill": "สกิลต่อสู้",
"maze": "เทคนิค",
"ultra": "ท่าไม้ตาย",
"servantskill": "สกิล Memosprite",
"severaltalent": "พรสวรรค์ Memosprite",
"singleattack": "โจมตีเป้าหมายเดียว",
"enhance": "เสริมพลัง",
"summon": "อัญเชิญ",
"mazeattack": "โจมตีแบบเทคนิค",
"blast": "กระจาย",
"restore": "ฟื้นฟู",
"support": "สนับสนุน",
"aoeattack": "โจมตีหมู่",
"impair": "ป่วน",
"bounce": "ชิ่ง",
"active": "เปิดใช้งาน",
"defence": "ป้องกัน",
"inactive": "ปิดใช้งาน",
"maxAll": "อัปเกรดทั้งหมด",
"maxAllSuccess": "อัปเกรดสกิลถึงระดับสูงสุดสำเร็จ",
"maxAllFailed": "อัปเกรดสกิลล้มเหลว",
"noRelicEquipped": "ไม่ได้สวมใส่รีลิกส์",
"anomalyArbitration": "Anomaly Arbitration",
"normalMode": "โหมดปกติ",
"hardMode": "โหมดยาก",
"selectPEAKEvent": "เลือกอีเวนต์ PEAK",
"mode": "โหมด",
"selectMode": "เลือกโหมด",
"rollBack": "ย้อนกลับ",
"upRoll": "เพิ่มอัปเกรด",
"downRoll": "ลดอัปเกรด",
"actions": "แอ็กชัน",
"avatars": "อวตาร",
"quickView": "ดูแบบด่วน",
"extraSetting": "การตั้งค่าเพิ่มเติม",
"disableCensorship": "ปิดการเซ็นเซอร์",
"hideUI": "ซ่อน UI",
"theoryCraftMode": "โหมด Theorycraft",
"cycleCount": "จำนวนรอบ",
"pleaseSelectAllSubStats": "โปรดเลือกสเตตัสรองทั้งหมด",
"subStatRollCountCannotBeZero": "จำนวนการอัปเกรดสเตตัสรองต้องไม่เป็นศูนย์",
"theoryCraft": "Theorycraft",
"multipathCharacter": "ตัวละครหลาย Path",
"mainPath": "Path หลัก",
"march7Path": "Path March 7th",
"challenge": "ท้าทาย",
"skipNode": "ข้ามโหนด",
"disableSkip": "ปิดใช้งานการข้าม",
"skipNode1": "ข้ามโหนด 1",
"skipNode2": "ข้ามโหนด 2",
"extraFeatures": "คุณสมบัติพิเศษ",
"detailTheoryCraft": "การเปิดคุณสมบัตินี้จะช่วยให้คุณปรับแต่งจำนวนรอบและปรับ HP ของศัตรูได้",
"detailSkipNode": "ช่วยให้ข้าม (โหนด 1/โหนด 2) ใน Memory of Chaos หรือ Pure Fiction",
"detailChallengePeak": "อนุญาตให้เปลี่ยนฤดูกาล Peak ใน Anomaly ปัจจุบัน",
"detailHiddenUi": "ซ่อนอินเทอร์เฟซของเกม",
"detailDisableCensorship": "ปิดการเซ็นเซอร์ในเกม",
"detailMultipathCharacter": "อนุญาตให้เปลี่ยน Path ของตัวละครบางตัว",
"trailblazer": "ผู้บุกเบิก",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"extra": "พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง"
}
}
+288 -288
View File
@@ -1,289 +1,289 @@
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng",
"character": "Nhân vật",
"id": "ID",
"path": "Vận mệnh",
"rarity": "Độ hiếm",
"element": "Nguyên tố",
"technique": "Bí kỹ",
"talent": "Thiên phú",
"basic": "Đánh thường",
"skill": "Kỹ năng",
"ultimate": "Tuyệt kỹ",
"servant": "Phụ trợ",
"damage": "Sát thương",
"type": "Loại",
"warrior": "Hủy Diệt",
"knight": "Bảo Hộ",
"mage": "Tri Thức",
"priest": "Trù phú",
"rogue": "Săn Bắn",
"shaman": "Hòa Hợp",
"warlock": "Hư Vô",
"memory": "Ký Ức",
"elation": "Vui vẻ",
"fire": "Hỏa",
"ice": "Băng",
"imaginary": "Ảo Ảnh",
"physical": "Vật Lý",
"quantum": "Lượng Tử",
"thunder": "Lôi",
"wind": "Phong",
"hp": "HP",
"atk": "Tấn công",
"speed": "Tốc độ",
"critRate": "Tỷ lệ bạo",
"critDmg": "ST bạo",
"breakEffect": "sát thương kích phá",
"effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Tốc độ hồi năng lượng",
"effectHitRate": "Tỷ lệ trúng hiệu ứng",
"outgoingHealingBoost": "Tăng hồi phục",
"fireDmgBoost": "Tăng sát thương Hỏa",
"iceDmgBoost": "Tăng sát thương Băng",
"imaginaryDmgBoost": "Tăng sát thương Ảo Ảnh",
"physicalDmgBoost": "Tăng sát thương Vật Lý",
"quantumDmgBoost": "Tăng sát thương Lượng Tử",
"thunderDmgBoost": "Tăng sát thương Lôi",
"windDmgBoost": "Tăng sát thương Phong",
"pursued": "Sát thương thêm",
"true damage": "Sát thương chuẩn",
"elationdamage": "Sát thương vui vẻ",
"follow-up": "Sát thương phản kích",
"elemental damage": "Sát thương kích phá và siêu kích phá",
"dot": "Sát thương theo thời gian",
"qte": "Kỹ năng QTE",
"level": "Cấp độ",
"relics": "Thánh di vật",
"eidolons": "Tinh hồn",
"lightcones": "Nón ánh sáng",
"loadData": "Tải dữ liệu",
"exportData": "Xuất dữ liệu",
"connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối",
"unconnected": "Chưa kết nối",
"psConnection": "Kết nối PS",
"connectionType": "Loại kết nối",
"status": "Trạng thái",
"connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối",
"other": "Khác",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Cài đặt quái",
"serverUrl": "Địa chỉ server",
"privateType": "Loại riêng tư",
"local": "Cục bộ",
"server": "Máy chủ",
"username": "Tên người dùng",
"password": "Mật khẩu",
"placeholderServerUrl": "Nhập địa chỉ server",
"placeholderUsername": "Nhập tên người dùng",
"placeholderPassword": "Nhập mật khẩu",
"connectedSuccess": "Kết nối PS thành công",
"connectedFailed": "Kết nối PS thất bại",
"syncSuccess": "Đồng bộ dữ liệu với PS thành công",
"syncFailed": "Đồng bộ dữ liệu với PS thất bại",
"sync": "Đồng bộ",
"importSetting": "Cài đặt nhập",
"profile": "Hồ sơ",
"default": "Mặc định",
"copyProfiles": "Sao chép hồ sơ",
"addNewProfile": "Thêm hồ sơ mới",
"createNewProfile": "Tạo hồ sơ mới",
"editProfile": "Chỉnh sửa hồ sơ",
"placeholderProfileName": "Nhập tên hồ sơ",
"profileName": "Tên hồ sơ",
"create": "Tạo",
"update": "Cập nhật",
"characterInformation": "Thông tin nhân vật",
"skills": "Kỹ năng",
"showcaseCard": "Thẻ trưng bày",
"comingSoon": "Sắp ra mắt",
"characterName": "Tên nhân vật",
"placeholderCharacter": "Nhập tên nhân vật",
"characterSettings": "Cài đặt nhân vật",
"levelConfiguration": "Cấu hình cấp độ",
"characterLevel": "Cấp độ nhân vật",
"max": "Tối đa",
"ultimateEnergy": "Năng lượng tuyệt kỹ",
"currentEnergy": "Năng lượng hiện tại",
"setTo50": "Đặt thành 50%",
"battleConfiguration": "Cấu hình trận đấu",
"useTechnique": "Dùng bí kỹ",
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
"enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa",
"origin": "Nguyên gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng",
"placeholderLevel": "Nhập cấp độ",
"superimpositionRank": "Bậc chồng kỹ năng",
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
"changeLightcone": "Thay đổi nón ánh sáng",
"removeLightcone": "Gỡ nón ánh sáng",
"equipLightcone": "Trang bị nón ánh sáng",
"noLightconeEquipped": "Chưa trang bị nón ánh sáng",
"equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
"filter": "Lọc",
"selectedCharacters": "Nhân vật đã chọn",
"selectedProfiles": "Hồ sơ đã chọn",
"clearAll": "Xóa tất cả",
"selectAll": "Chọn tất cả",
"copy": "Sao chép",
"copied": "Đã sao chép",
"noAvatarSelected": "Chưa chọn nhân vật",
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép",
"pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
"pleaseEnterUid": "Vui lòng nhập UID",
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại",
"pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
"noDataToImport": "Không có dữ liệu để nhập",
"pleaseSelectAFile": "Vui lòng chọn một tệp",
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
"getData": "Lấy dữ liệu",
"import": "Nhập",
"freeSRImport": "Nhập FreeSR",
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
"pickAFile": "Chọn tệp",
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
"relicMaker": "Trình tạo Thánh Di Vật",
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
"relicSavedSuccessfully": "Lưu thánh di vật thành công",
"mainSettings": "Cài đặt chính",
"mainStat": "Chỉ số chính",
"set": "Bộ",
"pleaseSelectASet": "Vui lòng chọn một bộ",
"effectBonus": "Hiệu ứng cộng thêm",
"totalRoll": "Tổng số dòng",
"randomizeStats": "Ngẫu nhiên chỉ số",
"randomizeRolls": "Ngẫu nhiên số dòng",
"selectASubStat": "Chọn chỉ số phụ",
"selectASet": "Chọn một bộ",
"selectAMainStat": "Chọn chỉ số chính",
"save": "Lưu",
"reset": "Đặt lại toàn bộ",
"roll": "Số dòng",
"step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng",
"side": "Nửa trận",
"wave": "Đợt",
"stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau",
"firstNodeEnemies": "Địch Node 1",
"secondNodeEnemies": "Địch Node 2",
"thirdNodeEnemies": "Địch Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên",
"lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE",
"selectPEAKEvent": "Chọn sự kiện PEAK",
"selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff",
"selectStage": "Chọn màn",
"previous": "Trước",
"next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết",
"normal": "Đánh thường",
"bpskill": "Chiến kỹ",
"maze": "Bí kỹ",
"ultra": "Tuyệt kỹ",
"servantskill": "Kỹ năng vật triệu hồi",
"severaltalent": "Thiên phú vật triệu hồi",
"singleattack": "Tấn công đơn",
"enhance": "Cường hóa",
"summon": "Triệu hồi",
"blast": "Tấn công 3 mục tiêu",
"restore": "Hồi phục",
"support": "Hỗ trợ",
"aoeattack": "Tấn công đa mục tiêu",
"mazeattack": "Bí kỹ tấn công",
"impair": "Suy yếu",
"bounce": "Nảy bật",
"active": "Kích hoạt",
"inactive": "Không kích hoạt",
"defence": "Phòng thủ",
"maxAll": "Tối đa tất cả",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
"noRelicEquipped": "Không có di vật",
"anomalyArbitration": "Trọng tài dị tướng",
"normalMode": "Chế độ thường",
"hardMode": "Chế độ khó",
"mode": "Chế độ",
"selectMode": "Chọn chế độ",
"rollBack": "Quay lại bước trước",
"upRoll": "Tăng dòng",
"downRoll": "Giảm dòng",
"actions": "Hành động",
"avatars": "Nhân vật",
"quickView": "Xem nhanh",
"extraSetting": "Cài đặt bổ sung",
"disableCensorship": "Tắt kiểm duyệt",
"hideUI": "Ẩn giao diện",
"theoryCraftMode": "Chế độ Theory Craft",
"cycleCount": "Số vòng",
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
"theoryCraft": "Theory Craft",
"multipathCharacter": "Nhân vật đa Vận Mệnh",
"mainPath": "Vận Mệnh Nhân Vật Chính",
"march7Path": "Vận Mệnh March 7",
"challenge": "Thử thách",
"skipNode": "Bỏ qua node",
"disableSkip": "Tắt bỏ qua",
"skipNode1": "Bỏ qua node 1",
"skipNode2": "Bỏ qua node 2",
"extraFeatures": "Tính năng bổ sung",
"detailTheoryCraft": "Khi bật tính năng này sẽ cho phép tùy chỉnh số cycle và trong mục kẻ địch tủy chỉnh sẽ cho phép điều chỉnh số hp.",
"detailSkipNode": "Khi bật tính năng này sẽ cho phép bỏ qua (node 1/node 2) của Hồi ức hỗn độn hoặc Kể chuyện hư cấu.",
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
"trailblazer": "Nhà khai phá",
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
"extra": "Bổ sung",
"customLineup": "Đội hình tùy chỉnh"
}
{
"TabTitle": {
"title": "Firefly Tools",
"description": "Firefly tools by Firefly Shelter"
},
"DataPage": {
"skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng",
"character": "Nhân vật",
"id": "ID",
"path": "Vận mệnh",
"rarity": "Độ hiếm",
"element": "Nguyên tố",
"technique": "Bí kỹ",
"talent": "Thiên phú",
"basic": "Đánh thường",
"skill": "Kỹ năng",
"ultimate": "Tuyệt kỹ",
"servant": "Phụ trợ",
"damage": "Sát thương",
"type": "Loại",
"warrior": "Hủy Diệt",
"knight": "Bảo Hộ",
"mage": "Tri Thức",
"priest": "Trù phú",
"rogue": "Săn Bắn",
"shaman": "Hòa Hợp",
"warlock": "Hư Vô",
"memory": "Ký Ức",
"elation": "Vui vẻ",
"fire": "Hỏa",
"ice": "Băng",
"imaginary": "Ảo Ảnh",
"physical": "Vật Lý",
"quantum": "Lượng Tử",
"thunder": "Lôi",
"wind": "Phong",
"hp": "HP",
"atk": "Tấn công",
"speed": "Tốc độ",
"critRate": "Tỷ lệ bạo",
"critDmg": "ST bạo",
"breakEffect": "sát thương kích phá",
"effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Tốc độ hồi năng lượng",
"effectHitRate": "Tỷ lệ trúng hiệu ứng",
"outgoingHealingBoost": "Tăng hồi phục",
"fireDmgBoost": "Tăng sát thương Hỏa",
"iceDmgBoost": "Tăng sát thương Băng",
"imaginaryDmgBoost": "Tăng sát thương Ảo Ảnh",
"physicalDmgBoost": "Tăng sát thương Vật Lý",
"quantumDmgBoost": "Tăng sát thương Lượng Tử",
"thunderDmgBoost": "Tăng sát thương Lôi",
"windDmgBoost": "Tăng sát thương Phong",
"pursued": "Sát thương thêm",
"true damage": "Sát thương chuẩn",
"elationdamage": "Sát thương vui vẻ",
"follow-up": "Sát thương phản kích",
"elemental damage": "Sát thương kích phá và siêu kích phá",
"dot": "Sát thương theo thời gian",
"qte": "Kỹ năng QTE",
"level": "Cấp độ",
"relics": "Thánh di vật",
"eidolons": "Tinh hồn",
"lightcones": "Nón ánh sáng",
"loadData": "Tải dữ liệu",
"exportData": "Xuất dữ liệu",
"connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối",
"unconnected": "Chưa kết nối",
"psConnection": "Kết nối PS",
"connectionType": "Loại kết nối",
"status": "Trạng thái",
"connectPs": "Kết nối PS",
"disconnect": "Ngắt kết nối",
"other": "Khác",
"freeSr": "FreeSR",
"database": "Database",
"enka": "Enka",
"monsterSetting": "Cài đặt quái",
"serverUrl": "Địa chỉ server",
"privateType": "Loại riêng tư",
"local": "Cục bộ",
"server": "Máy chủ",
"username": "Tên người dùng",
"password": "Mật khẩu",
"placeholderServerUrl": "Nhập địa chỉ server",
"placeholderUsername": "Nhập tên người dùng",
"placeholderPassword": "Nhập mật khẩu",
"connectedSuccess": "Kết nối PS thành công",
"connectedFailed": "Kết nối PS thất bại",
"syncSuccess": "Đồng bộ dữ liệu với PS thành công",
"syncFailed": "Đồng bộ dữ liệu với PS thất bại",
"sync": "Đồng bộ",
"importSetting": "Cài đặt nhập",
"profile": "Hồ sơ",
"default": "Mặc định",
"copyProfiles": "Sao chép hồ sơ",
"addNewProfile": "Thêm hồ sơ mới",
"createNewProfile": "Tạo hồ sơ mới",
"editProfile": "Chỉnh sửa hồ sơ",
"placeholderProfileName": "Nhập tên hồ sơ",
"profileName": "Tên hồ sơ",
"create": "Tạo",
"update": "Cập nhật",
"characterInformation": "Thông tin nhân vật",
"skills": "Kỹ năng",
"showcaseCard": "Thẻ trưng bày",
"comingSoon": "Sắp ra mắt",
"characterName": "Tên nhân vật",
"placeholderCharacter": "Nhập tên nhân vật",
"characterSettings": "Cài đặt nhân vật",
"levelConfiguration": "Cấu hình cấp độ",
"characterLevel": "Cấp độ nhân vật",
"max": "Tối đa",
"ultimateEnergy": "Năng lượng tuyệt kỹ",
"currentEnergy": "Năng lượng hiện tại",
"setTo50": "Đặt thành 50%",
"battleConfiguration": "Cấu hình trận đấu",
"useTechnique": "Dùng bí kỹ",
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
"enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa",
"origin": "Nguyên gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng",
"placeholderLevel": "Nhập cấp độ",
"superimpositionRank": "Bậc chồng kỹ năng",
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
"changeLightcone": "Thay đổi nón ánh sáng",
"removeLightcone": "Gỡ nón ánh sáng",
"equipLightcone": "Trang bị nón ánh sáng",
"noLightconeEquipped": "Chưa trang bị nón ánh sáng",
"equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
"filter": "Lọc",
"selectedCharacters": "Nhân vật đã chọn",
"selectedProfiles": "Hồ sơ đã chọn",
"clearAll": "Xóa tất cả",
"selectAll": "Chọn tất cả",
"copy": "Sao chép",
"copied": "Đã sao chép",
"noAvatarSelected": "Chưa chọn nhân vật",
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép",
"pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
"pleaseEnterUid": "Vui lòng nhập UID",
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại",
"pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
"noDataToImport": "Không có dữ liệu để nhập",
"pleaseSelectAFile": "Vui lòng chọn một tệp",
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
"getData": "Lấy dữ liệu",
"import": "Nhập",
"freeSRImport": "Nhập FreeSR",
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
"pickAFile": "Chọn tệp",
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
"relicMaker": "Trình tạo Thánh Di Vật",
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
"relicSavedSuccessfully": "Lưu thánh di vật thành công",
"mainSettings": "Cài đặt chính",
"mainStat": "Chỉ số chính",
"set": "Bộ",
"pleaseSelectASet": "Vui lòng chọn một bộ",
"effectBonus": "Hiệu ứng cộng thêm",
"totalRoll": "Tổng số dòng",
"randomizeStats": "Ngẫu nhiên chỉ số",
"randomizeRolls": "Ngẫu nhiên số dòng",
"selectASubStat": "Chọn chỉ số phụ",
"selectASet": "Chọn một bộ",
"selectAMainStat": "Chọn chỉ số chính",
"save": "Lưu",
"reset": "Đặt lại toàn bộ",
"roll": "Số dòng",
"step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng",
"side": "Nửa trận",
"wave": "Đợt",
"stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau",
"firstNodeEnemies": "Địch Node 1",
"secondNodeEnemies": "Địch Node 2",
"thirdNodeEnemies": "Địch Node 3",
"firstNode": "Node 1",
"secondNode": "Node 2",
"thirdNode": "Node 3",
"turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên",
"lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE",
"selectPEAKEvent": "Chọn sự kiện PEAK",
"selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff",
"selectStage": "Chọn màn",
"previous": "Trước",
"next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết",
"normal": "Đánh thường",
"bpskill": "Chiến kỹ",
"maze": "Bí kỹ",
"ultra": "Tuyệt kỹ",
"servantskill": "Kỹ năng vật triệu hồi",
"severaltalent": "Thiên phú vật triệu hồi",
"singleattack": "Tấn công đơn",
"enhance": "Cường hóa",
"summon": "Triệu hồi",
"blast": "Tấn công 3 mục tiêu",
"restore": "Hồi phục",
"support": "Hỗ trợ",
"aoeattack": "Tấn công đa mục tiêu",
"mazeattack": "Bí kỹ tấn công",
"impair": "Suy yếu",
"bounce": "Nảy bật",
"active": "Kích hoạt",
"inactive": "Không kích hoạt",
"defence": "Phòng thủ",
"maxAll": "Tối đa tất cả",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
"noRelicEquipped": "Không có di vật",
"anomalyArbitration": "Trọng tài dị tướng",
"normalMode": "Chế độ thường",
"hardMode": "Chế độ khó",
"mode": "Chế độ",
"selectMode": "Chọn chế độ",
"rollBack": "Quay lại bước trước",
"upRoll": "Tăng dòng",
"downRoll": "Giảm dòng",
"actions": "Hành động",
"avatars": "Nhân vật",
"quickView": "Xem nhanh",
"extraSetting": "Cài đặt bổ sung",
"disableCensorship": "Tắt kiểm duyệt",
"hideUI": "Ẩn giao diện",
"theoryCraftMode": "Chế độ Theory Craft",
"cycleCount": "Số vòng",
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
"theoryCraft": "Theory Craft",
"multipathCharacter": "Nhân vật đa Vận Mệnh",
"mainPath": "Vận Mệnh Nhân Vật Chính",
"march7Path": "Vận Mệnh March 7",
"challenge": "Thử thách",
"skipNode": "Bỏ qua node",
"disableSkip": "Tắt bỏ qua",
"skipNode1": "Bỏ qua node 1",
"skipNode2": "Bỏ qua node 2",
"extraFeatures": "Tính năng bổ sung",
"detailTheoryCraft": "Khi bật tính năng này sẽ cho phép tùy chỉnh số cycle và trong mục kẻ địch tủy chỉnh sẽ cho phép điều chỉnh số hp.",
"detailSkipNode": "Khi bật tính năng này sẽ cho phép bỏ qua (node 1/node 2) của Hồi ức hỗn độn hoặc Kể chuyện hư cấu.",
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
"trailblazer": "Nhà khai phá",
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
"extra": "Bổ sung",
"customLineup": "Đội hình tùy chỉnh"
}
}
+13 -13
View File
@@ -1,13 +1,13 @@
@echo off
echo [INFO] Create folder src\zod if not exist...
if not exist src\zod (
mkdir src\zod
)
echo [INFO] Start convert file .ts from src\types to Zod schemas...
for %%f in (src\types\*.ts) do (
echo [ZOD] Chuyển %%f -> src\zod\%%~nf.zod.ts
npx ts-to-zod src\types\%%~nxf src\zod\%%~nf.zod.ts
)
echo [DONE] ✅ Done
@echo off
echo [INFO] Create folder src\zod if not exist...
if not exist src\zod (
mkdir src\zod
)
echo [INFO] Start convert file .ts from src\types to Zod schemas...
for %%f in (src\types\*.ts) do (
echo [ZOD] Chuyển %%f -> src\zod\%%~nf.zod.ts
npx ts-to-zod src\types\%%~nxf src\zod\%%~nf.zod.ts
)
echo [DONE] ✅ Done
+24 -24
View File
@@ -1,25 +1,25 @@
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
}
+70 -70
View File
@@ -1,79 +1,79 @@
import { NextRequest, NextResponse } from 'next/server'
import axios from 'axios'
import net from 'net'
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
let response
switch (method.toUpperCase()) {
+9 -9
View File
@@ -1,9 +1,9 @@
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
+444 -444
View File
@@ -1,445 +1,445 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function ActionBar() {
const router = useRouter()
const { avatarSelected } = useCurrentDataStore()
const { damageType, baseType } = useDetailDataStore()
const { setResetData } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore()
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList[avatar.profileSelect] || null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newListProfile = [...listProfile]
const newProfile = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
}
newListProfile.push(newProfile)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = [...listProfile]
newListProfile[profileEdit].profile_name = profileName;
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId == 0) return;
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
const newListProfile = [...listProfile]
newListProfile.splice(profileId, 1)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
}
}
const handleConnectOrSyncPS = async () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars").toUpperCase(),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
return (
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
<div className="flex flex-col justify-center w-full">
<div className="flex flex-wrap items-center gap-2 ">
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
{avatarSelected && (
<div className="flex flex-wrap items-center justify-start h-full w-full">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt={'damage type'}
unoptimized
crossOrigin="anonymous"
className="h-10 w-10 object-contain"
width={100}
height={100}
/>
<p className="text-center font-bold text-lg">
{transI18n(avatarSelected.BaseType.toLowerCase())}
</p>
<div className="text-center font-bold text-lg">{" / "}</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className={"font-bold text-lg"}
/>
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
</div>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-center md:dropdown-start">
<div
tabIndex={0}
role="button"
className="btn btn-warning border-info btn-soft gap-1"
>
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name}
</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
{listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}
{index === 0 && (
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
)}
</span>
</button>
{index !== 0 && (
<>
<button
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
<button
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
onClick={(e) => {
handleDeleteProfile(index, e)
}}
title="Delete Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
</>
)}
</li>
))}
<li className="border-t border-base-300 mt-2 pt-2 z-10">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("copyProfiles")}
</button>
<button
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
<button
onClick={() => {
setIsOpenAvatars(true)
handleShow("avatars_modal")
}}
className="col-span-1 btn btn-warning btn-sm w-full">
{transI18n("avatars")}
</button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
>
<li>
<button onClick={() => actionMove('')}>
{transI18n("characterInformation")}
</button>
</li>
<li>
<button onClick={() => actionMove('relics-info')}>
{transI18n("relics")}
</button>
</li>
<li>
<button onClick={() => actionMove('eidolons-info')}>
{transI18n("eidolons")}
</button>
</li>
<li>
<button onClick={() => actionMove('skills-info')}>
{transI18n("skills")}
</button>
</li>
<li>
<button onClick={() => actionMove('showcase-card')}>
{transI18n("showcaseCard")}
</button>
</li>
<li>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
{transI18n("characterInformation")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
{transI18n("relics")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
{transI18n("eidolons")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
{transI18n("skills")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
{transI18n("showcaseCard")}
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
{content}
</div>
</dialog>
))}
</div>
</div>
);
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function ActionBar() {
const router = useRouter()
const { avatarSelected } = useCurrentDataStore()
const { damageType, baseType } = useDetailDataStore()
const { setResetData } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore()
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList[avatar.profileSelect] || null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newListProfile = [...listProfile]
const newProfile = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
}
newListProfile.push(newProfile)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = [...listProfile]
newListProfile[profileEdit].profile_name = profileName;
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId == 0) return;
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
const newListProfile = [...listProfile]
newListProfile.splice(profileId, 1)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
}
}
const handleConnectOrSyncPS = async () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars").toUpperCase(),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
return (
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
<div className="flex flex-col justify-center w-full">
<div className="flex flex-wrap items-center gap-2 ">
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
{avatarSelected && (
<div className="flex flex-wrap items-center justify-start h-full w-full">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt={'damage type'}
unoptimized
crossOrigin="anonymous"
className="h-10 w-10 object-contain"
width={100}
height={100}
/>
<p className="text-center font-bold text-lg">
{transI18n(avatarSelected.BaseType.toLowerCase())}
</p>
<div className="text-center font-bold text-lg">{" / "}</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className={"font-bold text-lg"}
/>
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
</div>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-center md:dropdown-start">
<div
tabIndex={0}
role="button"
className="btn btn-warning border-info btn-soft gap-1"
>
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name}
</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
{listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}
{index === 0 && (
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
)}
</span>
</button>
{index !== 0 && (
<>
<button
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
<button
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
onClick={(e) => {
handleDeleteProfile(index, e)
}}
title="Delete Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
</>
)}
</li>
))}
<li className="border-t border-base-300 mt-2 pt-2 z-10">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("copyProfiles")}
</button>
<button
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
<button
onClick={() => {
setIsOpenAvatars(true)
handleShow("avatars_modal")
}}
className="col-span-1 btn btn-warning btn-sm w-full">
{transI18n("avatars")}
</button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
>
<li>
<button onClick={() => actionMove('')}>
{transI18n("characterInformation")}
</button>
</li>
<li>
<button onClick={() => actionMove('relics-info')}>
{transI18n("relics")}
</button>
</li>
<li>
<button onClick={() => actionMove('eidolons-info')}>
{transI18n("eidolons")}
</button>
</li>
<li>
<button onClick={() => actionMove('skills-info')}>
{transI18n("skills")}
</button>
</li>
<li>
<button onClick={() => actionMove('showcase-card')}>
{transI18n("showcaseCard")}
</button>
</li>
<li>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
{transI18n("characterInformation")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
{transI18n("relics")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
{transI18n("eidolons")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
{transI18n("skills")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
{transI18n("showcaseCard")}
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
{content}
</div>
</dialog>
))}
</div>
</div>
);
}
+140 -140
View File
@@ -1,140 +1,140 @@
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const {locale} = useLocaleStore()
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="grid grid-flow-row h-full auto-rows-max w-full">
<div className="h-full rounded-lg mx-2 py-2">
<div className="container">
<div className="flex flex-col h-full gap-2">
<div className="flex flex-col gap-2">
<div className="flex justify-center">
<input type="text"
placeholder={transI18n("placeholderCharacter")}
className="input input-bordered input-primary w-full"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
</div>
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
style={{
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
</div>
<div className="flex items-start h-full">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
{listAvatar.map((item, index) => (
<div key={index} onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const {locale} = useLocaleStore()
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="grid grid-flow-row h-full auto-rows-max w-full">
<div className="h-full rounded-lg mx-2 py-2">
<div className="container">
<div className="flex flex-col h-full gap-2">
<div className="flex flex-col gap-2">
<div className="flex justify-center">
<input type="text"
placeholder={transI18n("placeholderCharacter")}
className="input input-bordered input-primary w-full"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
</div>
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
style={{
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
</div>
<div className="flex items-start h-full">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
{listAvatar.map((item, index) => (
<div key={index} onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+83 -83
View File
@@ -1,83 +1,83 @@
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore();
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore()
return (
<li
className="z-10 flex flex-col items-center rounded-xl shadow-xl
bg-linear-to-br from-base-300 via-base-100 to-warning/70
transform transition-transform duration-300 ease-in-out
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<div className="relative w-full h-32 lg:h-26 xl:h-36">
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="rounded-md w-full h-full object-contain"
alt="ALT"
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.DamageType.toLowerCase()}
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, data)}
className="
w-full px-0.5
my-1
text-center font-bold text-shadow-white
leading-tight
wrap-break-word
text-sm sm:text-base 2xl:text-lg
"
/>
</li>
);
}
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore();
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore()
return (
<li
className="z-10 flex flex-col items-center rounded-xl shadow-xl
bg-linear-to-br from-base-300 via-base-100 to-warning/70
transform transition-transform duration-300 ease-in-out
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<div className="relative w-full h-32 lg:h-26 xl:h-36">
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="rounded-md w-full h-full object-contain"
alt="ALT"
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.DamageType.toLowerCase()}
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, data)}
className="
w-full px-0.5
my-1
text-center font-bold text-shadow-white
leading-tight
wrap-break-word
text-sm sm:text-base 2xl:text-lg
"
/>
</li>
);
}
+124 -124
View File
@@ -1,125 +1,125 @@
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onCharacterToggle(character)}
>
{/* Character Portrait */}
<div className="relative mb-4">
<div className="w-full h-48 rounded-lg overflow-hidden relative">
<Image
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
className="w-full h-full object-contain"
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
{/* Character Name and Level */}
<div className="w-full rounded-lg flex items-center justify-center mb-2">
<div className="text-center">
<ParseText className="text-lg font-bold"
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
locale={locale}
/>
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
</div>
</div>
{character.relics.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{character.relics.map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone.item_id && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
</div>
</div>
</div>
)}
</div>
);
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onCharacterToggle(character)}
>
{/* Character Portrait */}
<div className="relative mb-4">
<div className="w-full h-48 rounded-lg overflow-hidden relative">
<Image
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
className="w-full h-full object-contain"
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
{/* Character Name and Level */}
<div className="w-full rounded-lg flex items-center justify-center mb-2">
<div className="text-center">
<ParseText className="text-lg font-bold"
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
locale={locale}
/>
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
</div>
</div>
{character.relics.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{character.relics.map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone.item_id && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
</div>
</div>
</div>
)}
</div>
);
};
+53 -53
View File
@@ -1,53 +1,53 @@
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getLocaleName(locale, data.Name)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
hover:scale-105 cursor-pointer min-h-55"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
"from-blue-400 via-blue-600/70 to-blue-800/50"
}`}
>
<div className="relative w-full h-full">
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
</div>
</div>
<ParseText
locale={locale}
text={text}
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
/>
</li>
);
}
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getLocaleName(locale, data.Name)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
hover:scale-105 cursor-pointer min-h-55"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
"from-blue-400 via-blue-600/70 to-blue-800/50"
}`}
>
<div className="relative w-full h-full">
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
</div>
</div>
<ParseText
locale={locale}
text={text}
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
/>
</li>
);
}
+78 -78
View File
@@ -1,79 +1,79 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onProfileToggle(profile)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
</div>
</div>
</div>
)}
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
</div>
);
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onProfileToggle(profile)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
</div>
</div>
</div>
)}
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
</div>
);
};
+195 -195
View File
@@ -1,196 +1,196 @@
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
default: return 'border-gray-500 shadow-gray-500/50';
}
};
const getRarityName = (slot: string) => {
switch (slot) {
case '1': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HEAD.png"
alt="Head"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Head</h2>
</div>
);
case '2': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HAND.png"
alt="Hand"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/BODY.png"
alt="Body"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/FOOT.png"
alt="Foot"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/NECK.png"
alt="Neck"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/OBJECT.png"
alt="Object"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Link rope</h2>
</div>
);
default: return '';
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { avatars } = useUserDataStore()
const { selectedRelicSlot } = useRelicMakerStore()
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
return avatar.profileList[avatar.profileSelect].relics[slot];
}
return null;
}
return null;
}, [avatars, avatarId, slot]);
return (
<div>
{relicDetail ? (
<div
className="flex flex-col items-center cursor-pointer ">
<div
className={`
relative w-24 h-24 rounded-full
${getRarityColor(relicDetail.relic_id.toString()[0])}
shadow-xl
flex items-center justify-center
cursor-pointer transition-transform
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
`}
>
<span>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
<div className="mt-3 text-center">
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
</div>
</div>
) : (
<div
className="flex flex-col items-center cursor-pointer">
<div
className={`
relative w-24 h-24 rounded-full border-4
${getRarityColor("None")}
bg-base-300 shadow-xl
flex items-center justify-center
cursor-pointer hover:scale-105 transition-transform
ring-4 ring-primary
`}
>
<span className="text-3xl">
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{0}</span>
</div>
</div>
<div className="mt-3 text-center">
<div>{getRarityName(slot)}</div>
</div>
</div>
)}
</div>
)
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
default: return 'border-gray-500 shadow-gray-500/50';
}
};
const getRarityName = (slot: string) => {
switch (slot) {
case '1': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HEAD.png"
alt="Head"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Head</h2>
</div>
);
case '2': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HAND.png"
alt="Hand"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/BODY.png"
alt="Body"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/FOOT.png"
alt="Foot"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/NECK.png"
alt="Neck"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/OBJECT.png"
alt="Object"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Link rope</h2>
</div>
);
default: return '';
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { avatars } = useUserDataStore()
const { selectedRelicSlot } = useRelicMakerStore()
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
return avatar.profileList[avatar.profileSelect].relics[slot];
}
return null;
}
return null;
}, [avatars, avatarId, slot]);
return (
<div>
{relicDetail ? (
<div
className="flex flex-col items-center cursor-pointer ">
<div
className={`
relative w-24 h-24 rounded-full
${getRarityColor(relicDetail.relic_id.toString()[0])}
shadow-xl
flex items-center justify-center
cursor-pointer transition-transform
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
`}
>
<span>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
<div className="mt-3 text-center">
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
</div>
</div>
) : (
<div
className="flex flex-col items-center cursor-pointer">
<div
className={`
relative w-24 h-24 rounded-full border-4
${getRarityColor("None")}
bg-base-300 shadow-xl
flex items-center justify-center
cursor-pointer hover:scale-105 transition-transform
ring-4 ring-primary
`}
>
<span className="text-3xl">
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{0}</span>
</div>
</div>
<div className="mt-3 text-center">
<div>{getRarityName(slot)}</div>
</div>
</div>
)}
</div>
)
}
+68 -68
View File
@@ -1,69 +1,69 @@
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore()
return (
<div
onClick={onClick}
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<Image
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain"
alt="Avatar"
/>
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</div>
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
{showRemoveHover && (
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
)}
</div>
);
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore()
return (
<div
onClick={onClick}
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<Image
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain"
alt="Avatar"
/>
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</div>
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
{showRemoveHover && (
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
)}
</div>
);
};
+90 -90
View File
@@ -1,91 +1,91 @@
import useLocaleStore from '@/stores/localeStore';
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore()
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
case 'fix':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
case 'improvement':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
default:
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'badge-success';
case 'fix':
return 'badge-error';
case 'improvement':
return 'badge-warning';
default:
return 'badge-info';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="md:p-4">
{/* Alert */}
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
<AlertCircle className="w-6 h-6" />
</div> */}
{/* Timeline */}
<div className="flex flex-col gap-4">
{changelog.map((change, index) => (
<div key={index} className="bg-base-100 shadow-sm">
<div className="flex flex-col gap-2">
{/* Version Header */}
<div className="flex flex-row items-center gap-2 md:gap-3">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version}
</h2>
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-3">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3">
<div className="mt-1">
<Check className="w-5 h-5 text-success" />
</div>
<span className="text-base-content">{item}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
);
import useLocaleStore from '@/stores/localeStore';
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore()
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
case 'fix':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
case 'improvement':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
default:
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'badge-success';
case 'fix':
return 'badge-error';
case 'improvement':
return 'badge-warning';
default:
return 'badge-info';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="md:p-4">
{/* Alert */}
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
<AlertCircle className="w-6 h-6" />
</div> */}
{/* Timeline */}
<div className="flex flex-col gap-4">
{changelog.map((change, index) => (
<div key={index} className="bg-base-100 shadow-sm">
<div className="flex flex-col gap-2">
{/* Version Header */}
<div className="flex flex-row items-center gap-2 md:gap-3">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version}
</h2>
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-3">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3">
<div className="mt-1">
<Check className="w-5 h-5 text-success" />
</div>
<span className="text-base-content">{item}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
);
}
+55 -55
View File
@@ -1,56 +1,56 @@
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
}
+168 -168
View File
@@ -1,169 +1,169 @@
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
)
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
)
}
+85 -85
View File
@@ -1,85 +1,85 @@
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore()
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
return (
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("eidolons")}
</h2>
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
{charRank && avatars[avatarSelected?.ID || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(charRank || {}).map(([key, rank]) => (
<div key={key}
className="flex flex-col items-center"
>
<div
className="cursor-pointer"
onClick={() => {
let newRank = Number(rank.Rank)
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
newRank = Number(rank.Rank) - 1
}
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
}}
>
<Image
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
<ParseText
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="text-center text-base font-normal leading-tight"
/>
</div>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
<ExtraEffectList extras={rank?.Extra} locale={locale} />
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore()
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
return (
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("eidolons")}
</h2>
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
{charRank && avatars[avatarSelected?.ID || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(charRank || {}).map(([key, rank]) => (
<div key={key}
className="flex flex-col items-center"
>
<div
className="cursor-pointer"
onClick={() => {
let newRank = Number(rank.Rank)
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
newRank = Number(rank.Rank) - 1
}
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
}}
>
<Image
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
<ParseText
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="text-center text-base font-normal leading-tight"
/>
</div>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
<ExtraEffectList extras={rank?.Extra} locale={locale} />
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
+98 -98
View File
@@ -1,99 +1,99 @@
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,9 +1,9 @@
export default function Footer() {
return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside>
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
</aside>
</footer>
)
}
export default function Footer() {
return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside>
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
</aside>
</footer>
)
}
File diff suppressed because it is too large Load Diff
+265 -265
View File
@@ -1,74 +1,74 @@
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
const { avatarSelected } = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
const { avatarSelected } = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
const selectAll = () => {
if (avatarCopySelected) {
const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
@@ -79,32 +79,32 @@ export default function CopyImport() {
return null;
}
return {
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
if (!avatarSelected) {
setMessage({
type: "error",
@@ -130,180 +130,180 @@ export default function CopyImport() {
return null;
}
return {
...profile,
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
} as AvatarProfileStore
}).filter((profile) => profile !== null);
...profile,
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
} as AvatarProfileStore
}).filter((profile) => profile !== null);
const newAvatar = {
...targetAvatar,
profileList: targetAvatar.profileList.concat(newListProfile),
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
}
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-lg px-2 min-h-[60vh]">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-2">
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>{transI18n("filter")}</div>
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
{/* Path */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListPath({ ...listPath, [key]: !listPath[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-8 w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Element */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListElement({ ...listElement, [key]: !listElement[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Rank */}
<div>
<div className="flex flex-wrap gap-2 justify-end items-center">
{Object.entries(listRank).map(([key], index) => (
<div
key={index}
onClick={() => {
setListRank({ ...listRank, [key]: !listRank[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
}}>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
{listAvatar.length > 0 && (
<div>
<div>{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
</div>
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedProfiles")}: {selectedProfiles.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
onClick={handleCopy}
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
{transI18n("copy")}
</button>
)}
</div>
{message.type && message.text && (
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
)}
</div>
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-lg px-2 min-h-[60vh]">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-2">
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>{transI18n("filter")}</div>
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
{/* Path */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListPath({ ...listPath, [key]: !listPath[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-8 w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Element */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListElement({ ...listElement, [key]: !listElement[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Rank */}
<div>
<div className="flex flex-wrap gap-2 justify-end items-center">
{Object.entries(listRank).map(([key], index) => (
<div
key={index}
onClick={() => {
setListRank({ ...listRank, [key]: !listRank[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
}}>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
{listAvatar.length > 0 && (
<div>
<div>{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
</div>
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedProfiles")}: {selectedProfiles.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
onClick={handleCopy}
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
{transI18n("copy")}
</button>
)}
</div>
{message.type && message.text && (
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
)}
</div>
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
}
+124 -124
View File
@@ -1,10 +1,10 @@
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
import useModelStore from "@/stores/modelStore";
import { toast } from "react-toastify";
@@ -31,24 +31,24 @@ const toCharacterInfoCard = (character: AvatarEnkaDetailInput): CharacterInfoCar
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
})),
});
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const { mapAvatar } = useDetailDataStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
const handlerFetchData = async () => {
if (!uidInput) {
setError(transI18n("pleaseEnterUid"))
@@ -77,36 +77,36 @@ export default function EnkaImport() {
setIsLoading(false)
}
}
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
if (enkaData) {
setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const parsed = enkaResponseSchema.safeParse(enkaData)
if (!parsed.success) {
setError(transI18n("failedToFetchEnkaData"));
@@ -125,80 +125,80 @@ export default function EnkaImport() {
acc[skill.pointId] = skill.level;
return acc;
}, {} as Record<string, number>),
}
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
<div className="flex gap-4 items-center mb-6">
<input
type="text"
value={uidInput}
onChange={(e) => setUidInput(e.target.value)}
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
placeholder="Enter UID"
/>
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="text-white space-y-2">
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
}
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
<div className="flex gap-4 items-center mb-6">
<input
type="text"
value={uidInput}
onChange={(e) => setUidInput(e.target.value)}
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
placeholder="Enter UID"
/>
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="text-white space-y-2">
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validCharacters.map((character) => (
<CharacterInfoCard
key={character.avatarId}
@@ -206,9 +206,9 @@ export default function EnkaImport() {
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
))}
</div>
</div>
</div>
);
))}
</div>
</div>
</div>
);
}
+134 -134
View File
@@ -1,15 +1,15 @@
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { z } from "zod";
@@ -36,55 +36,55 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
})),
}
}
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore();
const { mapAvatar } = useDetailDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore();
const { mapAvatar } = useDetailDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
const transI18n = useTranslations("DataPage")
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (parsed.success) {
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
}
};
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
if (file) {
const reader = new FileReader();
@@ -121,18 +121,18 @@ export default function FreeSRImport() {
reader.readAsText(file);
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (!parsed.success) {
setError(transI18n("fileMustBeAValidJsonFile"));
@@ -144,73 +144,73 @@ export default function FreeSRImport() {
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
newAvatar.promotion = character.promotion
newAvatar.data = {
rank: character.data.rank ?? 0,
skills: character.data.skills
}
newAvatar.level = character.level
newAvatar.promotion = character.promotion
newAvatar.data = {
rank: character.data.rank ?? 0,
skills: character.data.skills
}
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
<div className="flex gap-4 items-center mb-6">
<fieldset className="fieldset">
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
<div className="flex gap-4 items-center mb-6">
<fieldset className="fieldset">
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
onClick={clearSelection}
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
>
{transI18n("clearAll")}
</button>
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
</div>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
return (
<CharacterInfoCard
@@ -219,10 +219,10 @@ export default function FreeSRImport() {
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
)
})}
</div>
</div>
</div>
)
)
})}
</div>
</div>
</div>
)
}
+144 -144
View File
@@ -1,145 +1,145 @@
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
export default function LightconeBar() {
const { locale } = useLocaleStore()
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { setIsOpenLightcone } = useModelStore()
const { mapLightCone, baseType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item =>
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || mapLightconePathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("lightConeSetting")}
</h3>
</div>
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2">
<div>Search</div>
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
/>
</div>
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>Filter</div>
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.keys(mapLightconeRankActive).map((key, index) => (
<div
key={index}
onClick={() => {
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
}}
>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
{key}*
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
{listLightcone.map((item, index) => (
<div key={index} onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected?.ID?.toString()]
avatar.profileList[avatar.profileSelect].lightcone = {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
setAvatar({ ...avatar })
setIsOpenLightcone(false)
}
}}>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
export default function LightconeBar() {
const { locale } = useLocaleStore()
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { setIsOpenLightcone } = useModelStore()
const { mapLightCone, baseType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item =>
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || mapLightconePathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("lightConeSetting")}
</h3>
</div>
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2">
<div>Search</div>
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
/>
</div>
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>Filter</div>
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.keys(mapLightconeRankActive).map((key, index) => (
<div
key={index}
onClick={() => {
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
}}
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
}}
>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
{key}*
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
{listLightcone.map((item, index) => (
<div key={index} onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected?.ID?.toString()]
avatar.profileList[avatar.profileSelect].lightcone = {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
setAvatar({ ...avatar })
setIsOpenLightcone(false)
}
}}>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
}
+340 -340
View File
@@ -1,341 +1,341 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { ASEvent, MonsterStore } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
export default function AsBar() {
const { locale } = useLocaleStore()
const {
as_config,
setAsConfig
} = useUserDataStore()
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapAS[as_config.event_id.toString()]?.Level.find((as) => as.ID === as_config.challenge_id)
}, [as_config, mapAS])
const eventSelected = useMemo(() => {
return mapAS[as_config.event_id.toString()]
}, [as_config, mapAS])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === as_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [as_config.challenge_id, eventSelected, transI18n])
const buffList = useMemo(() => {
if (!eventSelected) return [];
if (as_config.floor_side === "firstNode") {
return eventSelected?.BuffList1 ?? [];
}
if (as_config.floor_side === "secondNode") {
return eventSelected?.BuffList2 ?? [];
}
if (as_config.floor_side === "thirdNode" && eventSelected?.BuffList3) {
return eventSelected?.BuffList3 ?? [];
}
return [];
}, [as_config.floor_side, eventSelected]);
useEffect(() => {
if (!challengeSelected || as_config.event_id === 0 || as_config.challenge_id === 0) return
const newBattleConfig = structuredClone(as_config)
newBattleConfig.cycle_count = challengeSelected.TurnLimit
newBattleConfig.blessings = []
if (as_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: as_config.buff_id,
level: 1
})
}
if (challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: ASEvent[] = []
if (as_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (as_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (as_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setAsConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
mapAS,
as_config.event_id,
as_config.challenge_id,
as_config.floor_side,
as_config.buff_id,
])
if (!mapAS) 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(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
id: as.ID.toString(),
name: getLocaleName(locale, as.Name),
time: `${as.BeginTime} - ${as.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({
...as_config,
event_id: Number(id),
challenge_id: mapAS[Number(id)]?.Level.at(-1)?.ID || 0,
buff_id: 0
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={as_config.challenge_id}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{eventSelected?.Level.map((as) => (
<option key={as.ID} value={as.ID}>{getLocaleName(locale, as.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={as_config.floor_side}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => {
return <option key={side.id} value={side.id}>{side.name}</option>
})}
</select>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {as_config?.stage_id}</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={buffList.map((buff) => ({
id: buff.ID?.toString() || "",
name: getLocaleName(locale, buff?.Name) || "",
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={as_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(as_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, 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">
{Object.values(wave).map((waveValue, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[waveValue.toString()],
targetEvent?.EliteGroup,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[waveValue.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?.[waveValue.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-3 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?.[waveValue.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>
)
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { ASEvent, MonsterStore } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
export default function AsBar() {
const { locale } = useLocaleStore()
const {
as_config,
setAsConfig
} = useUserDataStore()
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapAS[as_config.event_id.toString()]?.Level.find((as) => as.ID === as_config.challenge_id)
}, [as_config, mapAS])
const eventSelected = useMemo(() => {
return mapAS[as_config.event_id.toString()]
}, [as_config, mapAS])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === as_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [as_config.challenge_id, eventSelected, transI18n])
const buffList = useMemo(() => {
if (!eventSelected) return [];
if (as_config.floor_side === "firstNode") {
return eventSelected?.BuffList1 ?? [];
}
if (as_config.floor_side === "secondNode") {
return eventSelected?.BuffList2 ?? [];
}
if (as_config.floor_side === "thirdNode" && eventSelected?.BuffList3) {
return eventSelected?.BuffList3 ?? [];
}
return [];
}, [as_config.floor_side, eventSelected]);
useEffect(() => {
if (!challengeSelected || as_config.event_id === 0 || as_config.challenge_id === 0) return
const newBattleConfig = structuredClone(as_config)
newBattleConfig.cycle_count = challengeSelected.TurnLimit
newBattleConfig.blessings = []
if (as_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: as_config.buff_id,
level: 1
})
}
if (challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: ASEvent[] = []
if (as_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (as_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (as_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setAsConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
mapAS,
as_config.event_id,
as_config.challenge_id,
as_config.floor_side,
as_config.buff_id,
])
if (!mapAS) 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(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
id: as.ID.toString(),
name: getLocaleName(locale, as.Name),
time: `${as.BeginTime} - ${as.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({
...as_config,
event_id: Number(id),
challenge_id: mapAS[Number(id)]?.Level.at(-1)?.ID || 0,
buff_id: 0
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={as_config.challenge_id}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{eventSelected?.Level.map((as) => (
<option key={as.ID} value={as.ID}>{getLocaleName(locale, as.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={as_config.floor_side}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => {
return <option key={side.id} value={side.id}>{side.name}</option>
})}
</select>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {as_config?.stage_id}</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={buffList.map((buff) => ({
id: buff.ID?.toString() || "",
name: getLocaleName(locale, buff?.Name) || "",
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={as_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(as_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, 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">
{Object.values(wave).map((waveValue, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[waveValue.toString()],
targetEvent?.EliteGroup,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[waveValue.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?.[waveValue.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-3 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?.[waveValue.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>
)
}
File diff suppressed because it is too large Load Diff
+99 -99
View File
@@ -1,100 +1,100 @@
import MocBar from "./moc";
import useUserDataStore from "@/stores/userDataStore";
import PfBar from "./pf";
import Image from "next/image";
import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
import PeakBar from "./peak";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const navItems = [
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
{ name: transI18n("anomalyArbitration"), icon: 'ChallengePeakIcon', value: 'PEAK' },
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
];
return (
<div className="min-h-screen">
{/* Header Navigation */}
<nav className="border-b border-warning/30 relative pb-2">
<div className="flex items-center justify-center">
{/* Mobile Select */}
<div className="block md:hidden w-full">
<select
className="select select-bordered w-full"
value={battle_type.toUpperCase()}
onChange={(e) => setBattleType(e.target.value.toUpperCase())}
>
{navItems.map((item) => (
<option key={item.name} value={item.value.toUpperCase()}>
{item.name}
</option>
))}
</select>
</div>
{/* Desktop Tabs */}
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
{navItems.map((item) => (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
/>
</span>
<span>{item.name}</span>
</button>
))}
</div>
</div>
</nav>
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("noEventSelected")}
</div>
)}
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
{battle_type.toUpperCase() === 'PF' && <PfBar />}
{battle_type.toUpperCase() === 'AS' && <AsBar />}
{battle_type.toUpperCase() === 'CE' && <CeBar />}
{battle_type.toUpperCase() === 'PEAK' && <PeakBar />}
{battle_type.toUpperCase() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}
</div>
)}
</div>
)
import MocBar from "./moc";
import useUserDataStore from "@/stores/userDataStore";
import PfBar from "./pf";
import Image from "next/image";
import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
import PeakBar from "./peak";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const navItems = [
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
{ name: transI18n("anomalyArbitration"), icon: 'ChallengePeakIcon', value: 'PEAK' },
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
];
return (
<div className="min-h-screen">
{/* Header Navigation */}
<nav className="border-b border-warning/30 relative pb-2">
<div className="flex items-center justify-center">
{/* Mobile Select */}
<div className="block md:hidden w-full">
<select
className="select select-bordered w-full"
value={battle_type.toUpperCase()}
onChange={(e) => setBattleType(e.target.value.toUpperCase())}
>
{navItems.map((item) => (
<option key={item.name} value={item.value.toUpperCase()}>
{item.name}
</option>
))}
</select>
</div>
{/* Desktop Tabs */}
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
{navItems.map((item) => (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
/>
</span>
<span>{item.name}</span>
</button>
))}
</div>
</div>
</nav>
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("noEventSelected")}
</div>
)}
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
{battle_type.toUpperCase() === 'PF' && <PfBar />}
{battle_type.toUpperCase() === 'AS' && <AsBar />}
{battle_type.toUpperCase() === 'CE' && <CeBar />}
{battle_type.toUpperCase() === 'PEAK' && <PeakBar />}
{battle_type.toUpperCase() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}
</div>
)}
</div>
)
}
+341 -341
View File
@@ -1,342 +1,342 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MoCEvent, MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
export default function MocBar() {
const { locale } = useLocaleStore()
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapMoc[moc_config.event_id.toString()]?.Level.find((moc) => moc.ID === moc_config.challenge_id)
}, [moc_config, mapMoc])
const eventSelected = useMemo(() => {
return mapMoc[moc_config.event_id.toString()]
}, [moc_config, mapMoc])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === moc_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [moc_config.challenge_id, eventSelected, transI18n])
useEffect(() => {
if (!challengeSelected || moc_config.event_id === 0 || moc_config.challenge_id === 0) return
const newBattleConfig = structuredClone(moc_config)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challengeSelected.TurnLimit
}
newBattleConfig.blessings = []
if (moc_config.use_turbulence_buff && challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: MoCEvent[] = []
if (moc_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (moc_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (moc_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setMocConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
moc_config.event_id,
moc_config.challenge_id,
moc_config.floor_side,
moc_config.use_cycle_count,
moc_config.use_turbulence_buff,
mapMoc,
])
if (!mapMoc) 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(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
id: moc.ID.toString(),
name: getLocaleName(locale, moc.Name),
time: `${moc.BeginTime} - ${moc.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({
...moc_config,
event_id: Number(id),
challenge_id: mapMoc[Number(id)]?.Level.at(-1)?.ID || 0,
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={moc_config.challenge_id}
className="select select-success"
onChange={(e) => setMocConfig({
...moc_config,
challenge_id: Number(e.target.value)
})}
>
<option value={0} disabled={true}>Select a Floor</option>
{eventSelected?.Level?.map((moc) => (
<option key={moc.ID} value={moc.ID}>{getLocaleName(locale, moc.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={moc_config.floor_side}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => (
<option key={side.id} value={side.id}>{side.name}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label cursor-pointer">
<span
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
className="label-text font-bold text-success cursor-pointer"
>
{transI18n("useCycleCount")} {" "}
</span>
<input
type="checkbox"
checked={moc_config.use_cycle_count}
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
className="checkbox checkbox-primary"
/>
</label>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
className="checkbox checkbox-primary"
/>
<span
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
className="font-bold text-success cursor-pointer">
{transI18n("useTurbulenceBuff")}
</span>
</div>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(moc_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, 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">
{Object.values(wave).map((waveValue, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[waveValue.toString()],
targetEvent?.EliteGroup,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[waveValue.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?.[waveValue.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?.[waveValue.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>
)
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MoCEvent, MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
export default function MocBar() {
const { locale } = useLocaleStore()
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapMoc[moc_config.event_id.toString()]?.Level.find((moc) => moc.ID === moc_config.challenge_id)
}, [moc_config, mapMoc])
const eventSelected = useMemo(() => {
return mapMoc[moc_config.event_id.toString()]
}, [moc_config, mapMoc])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === moc_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [moc_config.challenge_id, eventSelected, transI18n])
useEffect(() => {
if (!challengeSelected || moc_config.event_id === 0 || moc_config.challenge_id === 0) return
const newBattleConfig = structuredClone(moc_config)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challengeSelected.TurnLimit
}
newBattleConfig.blessings = []
if (moc_config.use_turbulence_buff && challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: MoCEvent[] = []
if (moc_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (moc_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (moc_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setMocConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
moc_config.event_id,
moc_config.challenge_id,
moc_config.floor_side,
moc_config.use_cycle_count,
moc_config.use_turbulence_buff,
mapMoc,
])
if (!mapMoc) 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(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
id: moc.ID.toString(),
name: getLocaleName(locale, moc.Name),
time: `${moc.BeginTime} - ${moc.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({
...moc_config,
event_id: Number(id),
challenge_id: mapMoc[Number(id)]?.Level.at(-1)?.ID || 0,
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={moc_config.challenge_id}
className="select select-success"
onChange={(e) => setMocConfig({
...moc_config,
challenge_id: Number(e.target.value)
})}
>
<option value={0} disabled={true}>Select a Floor</option>
{eventSelected?.Level?.map((moc) => (
<option key={moc.ID} value={moc.ID}>{getLocaleName(locale, moc.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={moc_config.floor_side}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => (
<option key={side.id} value={side.id}>{side.name}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label cursor-pointer">
<span
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
className="label-text font-bold text-success cursor-pointer"
>
{transI18n("useCycleCount")} {" "}
</span>
<input
type="checkbox"
checked={moc_config.use_cycle_count}
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
className="checkbox checkbox-primary"
/>
</label>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
className="checkbox checkbox-primary"
/>
<span
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
className="font-bold text-success cursor-pointer">
{transI18n("useTurbulenceBuff")}
</span>
</div>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(moc_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
{targetEvent?.MonsterList?.map((wave, 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">
{Object.values(wave).map((waveValue, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[waveValue.toString()],
targetEvent?.EliteGroup,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[waveValue.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?.[waveValue.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?.[waveValue.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>
)
}
+307 -307
View File
@@ -1,308 +1,308 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";;
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
export default function PeakBar() {
const { locale } = useLocaleStore()
const {
peak_config,
setPeakConfig
} = useUserDataStore()
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()]
if (!peak) return []
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
}, [peak_config, mapPeak])
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]
}, [peak_config, mapPeak])
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak])
const challengeSelected = useMemo(() => {
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
if (
challenge
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
&& bossConfig
&& peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true }
}
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
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" : ""}
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}>{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>
)
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";;
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
export default function PeakBar() {
const { locale } = useLocaleStore()
const {
peak_config,
setPeakConfig
} = useUserDataStore()
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()]
if (!peak) return []
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
}, [peak_config, mapPeak])
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]
}, [peak_config, mapPeak])
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak])
const challengeSelected = useMemo(() => {
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
if (
challenge
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
&& bossConfig
&& peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true }
}
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
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" : ""}
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}>{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>
)
}
+340 -340
View File
@@ -1,341 +1,341 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { MonsterStore, PFEvent } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
export default function PfBar() {
const { locale } = useLocaleStore()
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapPF[pf_config.event_id.toString()]?.Level.find((pf) => pf.ID === pf_config.challenge_id)
}, [pf_config, mapPF])
const eventSelected = useMemo(() => {
return mapPF[pf_config.event_id.toString()]
}, [pf_config, mapPF])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === pf_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [pf_config.challenge_id, eventSelected, transI18n])
useEffect(() => {
if (!challengeSelected || pf_config.event_id === 0 || pf_config.challenge_id === 0) {
return
}
const newBattleConfig = structuredClone(pf_config)
newBattleConfig.cycle_count = challengeSelected.TurnLimit
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: pf_config.buff_id,
level: 1
})
}
if (challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: PFEvent[] = []
if (pf_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (pf_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (pf_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value as number,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setPfConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
pf_config.event_id,
pf_config.challenge_id,
pf_config.floor_side,
pf_config.buff_id,
mapPF,
])
if (!mapPF) 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(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
id: pf.ID.toString(),
name: getLocaleName(locale, pf.Name),
time: `${pf.BeginTime} - ${pf.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({
...pf_config,
event_id: Number(id),
challenge_id: mapPF[Number(id)]?.Level.slice(-1)[0]?.ID || 0,
buff_id: 0
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={pf_config.challenge_id}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{eventSelected?.Level.map((pf) => (
<option key={pf.ID} value={pf.ID}>{getLocaleName(locale, pf.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={pf_config.floor_side}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => (
<option key={side.id} value={side.id}>{side.name}</option>
))}
</select>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {pf_config?.stage_id}</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={eventSelected.Option.map((buff) => ({
id: buff.ID?.toString() || "",
name: getLocaleName(locale, buff?.Name) || "",
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={pf_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc) || "",
subOption.Param || []
)
}}
/>
</div>
))
) : eventSelected && challengeSelected && eventSelected.SubOption.length === 0 ? (
challengeSelected?.MazeBuff?.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(pf_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
{targetEvent && Object.values(targetEvent.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,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.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>
)
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { MonsterStore, PFEvent } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
export default function PfBar() {
const { locale } = useLocaleStore()
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapPF[pf_config.event_id.toString()]?.Level.find((pf) => pf.ID === pf_config.challenge_id)
}, [pf_config, mapPF])
const eventSelected = useMemo(() => {
return mapPF[pf_config.event_id.toString()]
}, [pf_config, mapPF])
const floorSideList = useMemo(() => {
if (!eventSelected) return [];
const floorList = [
{
id: "firstNode",
name: transI18n("firstNode"),
wave: transI18n("firstNodeEnemies")
},
{
id: "secondNode",
name: transI18n("secondNode"),
wave: transI18n("secondNodeEnemies")
},
]
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === pf_config.challenge_id) {
floorList.push({
id: "thirdNode",
name: transI18n("thirdNode"),
wave: transI18n("thirdNodeEnemies")
})
}
return floorList
}, [pf_config.challenge_id, eventSelected, transI18n])
useEffect(() => {
if (!challengeSelected || pf_config.event_id === 0 || pf_config.challenge_id === 0) {
return
}
const newBattleConfig = structuredClone(pf_config)
newBattleConfig.cycle_count = challengeSelected.TurnLimit
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: pf_config.buff_id,
level: 1
})
}
if (challengeSelected) {
challengeSelected.MazeBuff.map((item) => {
newBattleConfig.blessings.push({
id: item.ID,
level: 1
})
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
let targetEventList: PFEvent[] = []
if (pf_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
targetEventList = challengeSelected.EventList1
} else if (pf_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
targetEventList = challengeSelected.EventList2
} else if (pf_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
targetEventList = eventSelected.Tierce.EventList
}
if (targetEventList.length > 0) {
newBattleConfig.stage_id = targetEventList[0].ID
for (const wave of targetEventList[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: value as number,
level: targetEventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setPfConfig(newBattleConfig)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
challengeSelected,
eventSelected,
pf_config.event_id,
pf_config.challenge_id,
pf_config.floor_side,
pf_config.buff_id,
mapPF,
])
if (!mapPF) 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(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
id: pf.ID.toString(),
name: getLocaleName(locale, pf.Name),
time: `${pf.BeginTime} - ${pf.EndTime}`,
}))}
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({
...pf_config,
event_id: Number(id),
challenge_id: mapPF[Number(id)]?.Level.slice(-1)[0]?.ID || 0,
buff_id: 0
})}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={pf_config.challenge_id}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{eventSelected?.Level.map((pf) => (
<option key={pf.ID} value={pf.ID}>{getLocaleName(locale, pf.Name)}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={pf_config.floor_side}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
{floorSideList.map((side) => (
<option key={side.id} value={side.id}>{side.name}</option>
))}
</select>
</div>
</div>
<div className="label-text font-bold text-success mb-2">StageId: {pf_config?.stage_id}</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={eventSelected.Option.map((buff) => ({
id: buff.ID?.toString() || "",
name: getLocaleName(locale, buff?.Name) || "",
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={pf_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc) || "",
subOption.Param || []
)
}}
/>
</div>
))
) : eventSelected && challengeSelected && eventSelected.SubOption.length === 0 ? (
challengeSelected?.MazeBuff?.map((buff, i) => (
<div
key={i}
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, buff?.Desc) || "",
buff?.Param || []
)
}}
/>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(pf_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
const eventList = side.id === "firstNode"
? challengeSelected?.EventList1
: side.id === "secondNode"
? challengeSelected?.EventList2
: side.id === "thirdNode"
? eventSelected?.Tierce?.EventList
: [];
if (!eventList || eventList.length === 0) return null;
const targetEvent = eventList[0];
return (
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
{targetEvent && Object.values(targetEvent.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,
targetEvent?.HardLevelGroup,
targetEvent?.Level,
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. {targetEvent.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>
)
}
+15 -15
View File
@@ -1,16 +1,16 @@
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
}
+11 -11
View File
@@ -1,12 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
+472 -472
View File
@@ -1,473 +1,473 @@
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
export default function QuickView() {
const { avatars } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { avatarSelected, } = useCurrentDataStore()
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: "Elation Boost",
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt="Stat Icon"
width={40}
height={40}
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
/>
<div className="font-bold">{stat.name}</div>
</div>
<div className="ml-3 mr-3 grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
<hr />
</div>
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
<div>
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="flex flex-col items-center justify-center">
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
</div>
</div>
)}
</div>
</div>
)
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
export default function QuickView() {
const { avatars } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { avatarSelected, } = useCurrentDataStore()
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: "Elation Boost",
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt="Stat Icon"
width={40}
height={40}
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
/>
<div className="font-bold">{stat.name}</div>
</div>
<div className="ml-3 mr-3 grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
<hr />
</div>
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
<div>
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="flex flex-col items-center justify-center">
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
</div>
</div>
)}
</div>
</div>
)
}
+498 -498
View File
@@ -1,498 +1,498 @@
"use client";
import useUserDataStore from '@/stores/userDataStore';
import { useEffect, useMemo, useState } from 'react';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, calcMainAffixBonus, randomPartition, randomStep, replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from '@/constant/constant';
import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
import { toast } from 'react-toastify';
import { useTranslations } from 'next-intl'
import { ChevronDown, ChevronUp } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
import useDetailDataStore from '@/stores/detailDataStore';
import useCurrentDataStore from '@/stores/currentDataStore';
import { RelicSetDetail, SubAffixData } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import { mappingRelicSlot } from "@/constant/constant";
export default function RelicMaker() {
const { avatars, setAvatars } = useUserDataStore()
const { avatarSelected } = useCurrentDataStore()
const { setIsOpenRelic } = useModelStore()
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const {
selectedRelicSlot,
selectedRelicSet,
selectedMainStat,
listSelectedSubStats,
selectedRelicLevel,
preSelectedSubStats,
setSelectedRelicSet,
setSelectedMainStat,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
popHistory,
addHistory,
} = useRelicMakerStore()
const [error, setError] = useState<string>("");
const relicSets = useMemo(() => {
const listSet: Record<string, RelicSetDetail> = {};
for (const [key, value] of Object.entries(mapRelicSet || {})) {
let isOk = false;
for (const key2 of Object.keys(value.Parts)) {
if (key2 == mappingRelicSlot?.[selectedRelicSlot]) {
isOk = true;
break;
}
}
if (isOk) {
listSet[key] = value;
}
}
return listSet;
}, [mapRelicSet , selectedRelicSlot]);
const subAffixOptions = useMemo(() => {
const listSet: Record<string, SubAffixData> = {};
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot]
if (Object.keys(subAffixMap || {}).length === 0 || Object.keys(mainAffixMap || {}).length === 0) return listSet;
for (const [key, value] of Object.entries(subAffixMap)) {
if (value.Property !== mainAffixMap[selectedMainStat]?.Property) {
listSet[key] = value;
}
}
return listSet;
}, [subAffix, mainAffix, selectedRelicSlot, selectedMainStat]);
useEffect(() => {
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
if (!subAffixMap || !mainAffixMap) return;
const mainProp = mainAffixMap[selectedMainStat]?.Property;
if (!mainProp) return;
const newSubAffixes = structuredClone(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
if (newSubAffixes[i].property === mainProp) {
newSubAffixes[i].affixId = "";
newSubAffixes[i].property = "";
newSubAffixes[i].rollCount = 0;
newSubAffixes[i].stepCount = 0;
updated = true;
}
}
if (updated) setListSelectedSubStats(newSubAffixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMainStat, subAffix, mainAffix, selectedRelicSlot]);
const exSubAffixOptions = useMemo(() => {
const listSet: Record<string, SubAffixData> = {};
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
if (!subAffixMap || !mainAffixMap) return listSet;
for (const [key, value] of Object.entries(subAffixMap)) {
const subAffix = listSelectedSubStats.find((item) => item.property === value.Property);
if (subAffix && value.Property !== mainAffixMap[selectedMainStat]?.Property) {
listSet[key] = value;
}
}
return listSet;
}, [subAffix, listSelectedSubStats, mainAffix, selectedRelicSlot, selectedMainStat]);
const effectBonus = useMemo(() => {
const affixSet = mainAffix?.["5" + selectedRelicSlot];
if (!affixSet) return 0;
const data = affixSet[selectedMainStat];
if (!data) return 0;
return calcMainAffixBonus(data, selectedRelicLevel);
}, [mainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
setError("");
const newSubAffixes = structuredClone(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
newSubAffixes[index].rollCount = rollCount;
newSubAffixes[index].stepCount = stepCount;
setListSelectedSubStats(newSubAffixes);
addHistory(index, newSubAffixes[index]);
return;
}
newSubAffixes[index].affixId = key;
newSubAffixes[index].property = subAffixOptions[key].Property;
newSubAffixes[index].rollCount = rollCount;
newSubAffixes[index].stepCount = stepCount;
setListSelectedSubStats(newSubAffixes);
addHistory(index, newSubAffixes[index]);
};
const handlerRollback = (index: number) => {
setError("");
if (!preSelectedSubStats[index]) return;
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = structuredClone(listSelectedSubStats);
const listHistory = structuredClone(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
newSubAffixes[index].stepCount = preSubAffixes.stepCount;
setListSelectedSubStats(newSubAffixes);
popHistory(index);
};
const resetSubStat = (index: number) => {
const newSubAffixes = structuredClone(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
newSubAffixes[index].rollCount = 0;
newSubAffixes[index].stepCount = 0;
setListSelectedSubStats(newSubAffixes);
};
const randomizeStats = () => {
const newSubAffixes = structuredClone(listSelectedSubStats);
const exKeys = Object.keys(exSubAffixOptions);
for (let i = 0; i < newSubAffixes.length; i++) {
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
const randomKey = keys[Math.floor(Math.random() * keys.length)];
exKeys.push(randomKey);
const randomValue = subAffixOptions[randomKey];
newSubAffixes[i].affixId = randomKey;
newSubAffixes[i].property = randomValue.Property;
newSubAffixes[i].rollCount = 0;
newSubAffixes[i].stepCount = 0;
}
for (let i = 0; i < newSubAffixes.length; i++) {
addHistory(i, newSubAffixes[i]);
}
setListSelectedSubStats(newSubAffixes);
};
const randomizeRolls = () => {
const newSubAffixes = structuredClone(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];
newSubAffixes[i].stepCount = randomStep(randomRolls[i]);
}
setListSelectedSubStats(newSubAffixes);
for (let i = 0; i < newSubAffixes.length; i++) {
addHistory(i, newSubAffixes[i]);
}
};
const handlerSaveRelic = () => {
setError("");
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
if (!selectedRelicSet || !selectedMainStat || !selectedRelicLevel || !selectedRelicSlot) {
setError(transI18n("pleaseSelectAllOptions"));
return;
};
if (listSelectedSubStats.find((item) => item.affixId === "")) {
setError(transI18n("pleaseSelectAllSubStats"));
return;
};
if (avatar) {
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
level: selectedRelicLevel,
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
relic_set_id: Number(selectedRelicSet),
main_affix_id: Number(selectedMainStat),
sub_affixes: listSelectedSubStats.map((item) => {
return {
sub_affix_id: Number(item.affixId),
count: item.rollCount,
step: item.stepCount
}
})
}
}
setAvatars({ ...avatars });
setIsOpenRelic(false);
toast.success(transI18n("relicSavedSuccessfully"));
}
return (
<div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("relicMaker")}
</h3>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{/* Left Panel */}
<div className="space-y-6">
{/* Set Configuration */}
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{/* Main Stat */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
<SelectCustomImage
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
excludeSet={[]}
selectedCustomSet={selectedMainStat}
placeholder={transI18n("selectAMainStat")}
setSelectedCustomSet={setSelectedMainStat}
/>
</div>
{/* Relic Set Selection */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
<SelectCustomImage
customSet={Object.entries(relicSets).map(([key, value]) => ({
value: key,
label: getLocaleName(locale, value.Name),
imageUrl: `${process.env.CDN_URL}/${value.Image.SetIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={selectedRelicSet}
placeholder={transI18n("selectASet")}
setSelectedCustomSet={setSelectedRelicSet}
/>
</div>
</div>
{/* Set Bonus Display */}
<div className="mb-6 py-4 bg-base-100 rounded-lg">
{selectedRelicSet !== "" ? Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
<div key={key} className="text-blue-300 text-sm mb-1">
<span className="text-info font-bold">{key}-Pc:
<div
className="text-warning leading-relaxed font-bold"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/> </span>
</div>
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
</div>
{/* Rarity */}
<div className="grid grid-cols-2 items-center gap-4 mb-6">
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} </label>
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
</div>
{/* Level */}
<div className="mb-6">
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
<div className="bg-base-200 rounded-lg p-4">
<input
type="range"
min="0"
max="15"
value={selectedRelicLevel}
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
className="range range-primary w-full"
/>
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
</div>
</div>
<AnimatePresence>
{error && (
<motion.p
key="error"
initial={{ x: 0 }}
animate={{ x: [0, -2, 2, -2, 2, 0] }}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut",
repeatDelay: 1.5,
}}
className="text-error my-2 text-center font-bold"
>
{error}!
</motion.p>
)}
</AnimatePresence>
{/* Save Button */}
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
{transI18n("save")}
</button>
</div>
</div>
{/* Right Panel - Sub Stats */}
<div className="space-y-4">
{/* Total Roll */}
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
<div className="grid grid-cols-2 gap-2">
<button
className="btn btn-outline btn-success sm:btn-sm"
onClick={randomizeStats}
>
{transI18n("randomizeStats")}
</button>
<button
className="btn btn-outline btn-success sm:btn-sm"
onClick={randomizeRolls}
>
{transI18n("randomizeRolls")}
</button>
</div>
</div>
{listSelectedSubStats.map((v, index) => (
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
<div className="grid grid-cols-12 gap-2 items-center">
{/* Stat Selection */}
<div className="col-span-8">
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
selectedCustomSet={v.affixId}
placeholder={transI18n("selectASubStat")}
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
/>
</div>
{/* Current Value */}
<div className="col-span-4 text-center flex items-center justify-center gap-2">
<span className="text-2xl font-mono">+{ }</span>
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div>
</div>
{/* Up Roll Values */}
<div className="col-span-12">
<div className="flex items-center gap-2 mb-2">
<ChevronUp className="w-4 h-4 text-success" />
<span className="text-sm font-semibold text-success">{transI18n("upRoll")}</span>
</div>
<div className="grid grid-cols-3 gap-1">
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
</div>
</div>
{/* Down Roll Values */}
<div className="col-span-12">
<div className="flex items-center gap-2 mb-2">
<ChevronDown className="w-4 h-4 text-error" />
<span className="text-sm font-semibold text-error">{transI18n("downRoll")}</span>
</div>
<div className="grid grid-cols-3 gap-1">
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 1, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 1, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 2, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 2, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
</div>
</div>
{/* Reset Button & Roll Info */}
<div className="col-span-12 text-center w-full">
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full">
<div className="grid grid-cols-2 gap-2 items-center w-full">
<button
className="btn btn-error btn-sm mb-1"
onClick={() => resetSubStat(index)}
>
{transI18n("reset")}
</button>
<button
className="btn btn-warning btn-sm mb-1"
onClick={() => handlerRollback(index)}
>
{transI18n("rollBack")}
</button>
</div>
<div className="grid grid-cols-2 gap-2 items-center w-full">
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
"use client";
import useUserDataStore from '@/stores/userDataStore';
import { useEffect, useMemo, useState } from 'react';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, calcMainAffixBonus, randomPartition, randomStep, replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from '@/constant/constant';
import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
import { toast } from 'react-toastify';
import { useTranslations } from 'next-intl'
import { ChevronDown, ChevronUp } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
import useDetailDataStore from '@/stores/detailDataStore';
import useCurrentDataStore from '@/stores/currentDataStore';
import { RelicSetDetail, SubAffixData } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import { mappingRelicSlot } from "@/constant/constant";
export default function RelicMaker() {
const { avatars, setAvatars } = useUserDataStore()
const { avatarSelected } = useCurrentDataStore()
const { setIsOpenRelic } = useModelStore()
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const {
selectedRelicSlot,
selectedRelicSet,
selectedMainStat,
listSelectedSubStats,
selectedRelicLevel,
preSelectedSubStats,
setSelectedRelicSet,
setSelectedMainStat,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
popHistory,
addHistory,
} = useRelicMakerStore()
const [error, setError] = useState<string>("");
const relicSets = useMemo(() => {
const listSet: Record<string, RelicSetDetail> = {};
for (const [key, value] of Object.entries(mapRelicSet || {})) {
let isOk = false;
for (const key2 of Object.keys(value.Parts)) {
if (key2 == mappingRelicSlot?.[selectedRelicSlot]) {
isOk = true;
break;
}
}
if (isOk) {
listSet[key] = value;
}
}
return listSet;
}, [mapRelicSet , selectedRelicSlot]);
const subAffixOptions = useMemo(() => {
const listSet: Record<string, SubAffixData> = {};
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot]
if (Object.keys(subAffixMap || {}).length === 0 || Object.keys(mainAffixMap || {}).length === 0) return listSet;
for (const [key, value] of Object.entries(subAffixMap)) {
if (value.Property !== mainAffixMap[selectedMainStat]?.Property) {
listSet[key] = value;
}
}
return listSet;
}, [subAffix, mainAffix, selectedRelicSlot, selectedMainStat]);
useEffect(() => {
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
if (!subAffixMap || !mainAffixMap) return;
const mainProp = mainAffixMap[selectedMainStat]?.Property;
if (!mainProp) return;
const newSubAffixes = structuredClone(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
if (newSubAffixes[i].property === mainProp) {
newSubAffixes[i].affixId = "";
newSubAffixes[i].property = "";
newSubAffixes[i].rollCount = 0;
newSubAffixes[i].stepCount = 0;
updated = true;
}
}
if (updated) setListSelectedSubStats(newSubAffixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMainStat, subAffix, mainAffix, selectedRelicSlot]);
const exSubAffixOptions = useMemo(() => {
const listSet: Record<string, SubAffixData> = {};
const subAffixMap = subAffix["5"];
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
if (!subAffixMap || !mainAffixMap) return listSet;
for (const [key, value] of Object.entries(subAffixMap)) {
const subAffix = listSelectedSubStats.find((item) => item.property === value.Property);
if (subAffix && value.Property !== mainAffixMap[selectedMainStat]?.Property) {
listSet[key] = value;
}
}
return listSet;
}, [subAffix, listSelectedSubStats, mainAffix, selectedRelicSlot, selectedMainStat]);
const effectBonus = useMemo(() => {
const affixSet = mainAffix?.["5" + selectedRelicSlot];
if (!affixSet) return 0;
const data = affixSet[selectedMainStat];
if (!data) return 0;
return calcMainAffixBonus(data, selectedRelicLevel);
}, [mainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
setError("");
const newSubAffixes = structuredClone(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
newSubAffixes[index].rollCount = rollCount;
newSubAffixes[index].stepCount = stepCount;
setListSelectedSubStats(newSubAffixes);
addHistory(index, newSubAffixes[index]);
return;
}
newSubAffixes[index].affixId = key;
newSubAffixes[index].property = subAffixOptions[key].Property;
newSubAffixes[index].rollCount = rollCount;
newSubAffixes[index].stepCount = stepCount;
setListSelectedSubStats(newSubAffixes);
addHistory(index, newSubAffixes[index]);
};
const handlerRollback = (index: number) => {
setError("");
if (!preSelectedSubStats[index]) return;
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = structuredClone(listSelectedSubStats);
const listHistory = structuredClone(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
newSubAffixes[index].stepCount = preSubAffixes.stepCount;
setListSelectedSubStats(newSubAffixes);
popHistory(index);
};
const resetSubStat = (index: number) => {
const newSubAffixes = structuredClone(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
newSubAffixes[index].rollCount = 0;
newSubAffixes[index].stepCount = 0;
setListSelectedSubStats(newSubAffixes);
};
const randomizeStats = () => {
const newSubAffixes = structuredClone(listSelectedSubStats);
const exKeys = Object.keys(exSubAffixOptions);
for (let i = 0; i < newSubAffixes.length; i++) {
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
const randomKey = keys[Math.floor(Math.random() * keys.length)];
exKeys.push(randomKey);
const randomValue = subAffixOptions[randomKey];
newSubAffixes[i].affixId = randomKey;
newSubAffixes[i].property = randomValue.Property;
newSubAffixes[i].rollCount = 0;
newSubAffixes[i].stepCount = 0;
}
for (let i = 0; i < newSubAffixes.length; i++) {
addHistory(i, newSubAffixes[i]);
}
setListSelectedSubStats(newSubAffixes);
};
const randomizeRolls = () => {
const newSubAffixes = structuredClone(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];
newSubAffixes[i].stepCount = randomStep(randomRolls[i]);
}
setListSelectedSubStats(newSubAffixes);
for (let i = 0; i < newSubAffixes.length; i++) {
addHistory(i, newSubAffixes[i]);
}
};
const handlerSaveRelic = () => {
setError("");
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
if (!selectedRelicSet || !selectedMainStat || !selectedRelicLevel || !selectedRelicSlot) {
setError(transI18n("pleaseSelectAllOptions"));
return;
};
if (listSelectedSubStats.find((item) => item.affixId === "")) {
setError(transI18n("pleaseSelectAllSubStats"));
return;
};
if (avatar) {
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
level: selectedRelicLevel,
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
relic_set_id: Number(selectedRelicSet),
main_affix_id: Number(selectedMainStat),
sub_affixes: listSelectedSubStats.map((item) => {
return {
sub_affix_id: Number(item.affixId),
count: item.rollCount,
step: item.stepCount
}
})
}
}
setAvatars({ ...avatars });
setIsOpenRelic(false);
toast.success(transI18n("relicSavedSuccessfully"));
}
return (
<div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("relicMaker")}
</h3>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{/* Left Panel */}
<div className="space-y-6">
{/* Set Configuration */}
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{/* Main Stat */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
<SelectCustomImage
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
excludeSet={[]}
selectedCustomSet={selectedMainStat}
placeholder={transI18n("selectAMainStat")}
setSelectedCustomSet={setSelectedMainStat}
/>
</div>
{/* Relic Set Selection */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
<SelectCustomImage
customSet={Object.entries(relicSets).map(([key, value]) => ({
value: key,
label: getLocaleName(locale, value.Name),
imageUrl: `${process.env.CDN_URL}/${value.Image.SetIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={selectedRelicSet}
placeholder={transI18n("selectASet")}
setSelectedCustomSet={setSelectedRelicSet}
/>
</div>
</div>
{/* Set Bonus Display */}
<div className="mb-6 py-4 bg-base-100 rounded-lg">
{selectedRelicSet !== "" ? Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
<div key={key} className="text-blue-300 text-sm mb-1">
<span className="text-info font-bold">{key}-Pc:
<div
className="text-warning leading-relaxed font-bold"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/> </span>
</div>
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
</div>
{/* Rarity */}
<div className="grid grid-cols-2 items-center gap-4 mb-6">
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} </label>
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
</div>
{/* Level */}
<div className="mb-6">
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
<div className="bg-base-200 rounded-lg p-4">
<input
type="range"
min="0"
max="15"
value={selectedRelicLevel}
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
className="range range-primary w-full"
/>
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
</div>
</div>
<AnimatePresence>
{error && (
<motion.p
key="error"
initial={{ x: 0 }}
animate={{ x: [0, -2, 2, -2, 2, 0] }}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut",
repeatDelay: 1.5,
}}
className="text-error my-2 text-center font-bold"
>
{error}!
</motion.p>
)}
</AnimatePresence>
{/* Save Button */}
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
{transI18n("save")}
</button>
</div>
</div>
{/* Right Panel - Sub Stats */}
<div className="space-y-4">
{/* Total Roll */}
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
<div className="grid grid-cols-2 gap-2">
<button
className="btn btn-outline btn-success sm:btn-sm"
onClick={randomizeStats}
>
{transI18n("randomizeStats")}
</button>
<button
className="btn btn-outline btn-success sm:btn-sm"
onClick={randomizeRolls}
>
{transI18n("randomizeRolls")}
</button>
</div>
</div>
{listSelectedSubStats.map((v, index) => (
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
<div className="grid grid-cols-12 gap-2 items-center">
{/* Stat Selection */}
<div className="col-span-8">
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
value: key,
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
}))}
selectedCustomSet={v.affixId}
placeholder={transI18n("selectASubStat")}
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
/>
</div>
{/* Current Value */}
<div className="col-span-4 text-center flex items-center justify-center gap-2">
<span className="text-2xl font-mono">+{ }</span>
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div>
</div>
{/* Up Roll Values */}
<div className="col-span-12">
<div className="flex items-center gap-2 mb-2">
<ChevronUp className="w-4 h-4 text-success" />
<span className="text-sm font-semibold text-success">{transI18n("upRoll")}</span>
</div>
<div className="grid grid-cols-3 gap-1">
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
</div>
</div>
{/* Down Roll Values */}
<div className="col-span-12">
<div className="flex items-center gap-2 mb-2">
<ChevronDown className="w-4 h-4 text-error" />
<span className="text-sm font-semibold text-error">{transI18n("downRoll")}</span>
</div>
<div className="grid grid-cols-3 gap-1">
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 1, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 1, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 2, 0))}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 2, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
</div>
</div>
{/* Reset Button & Roll Info */}
<div className="col-span-12 text-center w-full">
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full">
<div className="grid grid-cols-2 gap-2 items-center w-full">
<button
className="btn btn-error btn-sm mb-1"
onClick={() => resetSubStat(index)}
>
{transI18n("reset")}
</button>
<button
className="btn btn-warning btn-sm mb-1"
onClick={() => handlerRollback(index)}
>
{transI18n("rollBack")}
</button>
</div>
<div className="grid grid-cols-2 gap-2 items-center w-full">
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
+366 -366
View File
@@ -1,366 +1,366 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore()
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = useRelicMakerStore()
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore()
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore()
const { mapRelicSet, subAffix } = useDetailDataStore()
const { locale } = useLocaleStore()
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
delete avatar.profileList[avatar.profileSelect].relics[slot]
setAvatars({ ...avatars });
}
}
const handlerChangeRelic = (slot: string) => {
const relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: "",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Section - Items Grid */}
<div className="lg:col-span-2">
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("relics")}
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div key={index} className="relative group">
<div
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
className="cursor-pointer"
>
<RelicCard
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-2 mt-10">
<button
disabled={!selectedRelicSlot}
onClick={() => {
handlerChangeRelic(selectedRelicSlot)
}}
className="btn btn-info"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button>
</div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div>
</div>
{/* Right Section - Stats and Set Effects */}
<div className="space-y-6">
{/* Set Effects Panel */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("setEffects")}
</h3>
<div className="space-y-6">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="space-y-3">
<div className="flex items-center gap-2">
<div
className="font-bold text-warning"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div>
<div className="space-y-2 pl-4">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
</motion.button>
</div>
{title && (
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
)}
{content}
</div>
</dialog>
))}
</div>
);
}
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore()
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = useRelicMakerStore()
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore()
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore()
const { mapRelicSet, subAffix } = useDetailDataStore()
const { locale } = useLocaleStore()
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
delete avatar.profileList[avatar.profileSelect].relics[slot]
setAvatars({ ...avatars });
}
}
const handlerChangeRelic = (slot: string) => {
const relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: "",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Section - Items Grid */}
<div className="lg:col-span-2">
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("relics")}
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div key={index} className="relative group">
<div
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
className="cursor-pointer"
>
<RelicCard
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-2 mt-10">
<button
disabled={!selectedRelicSlot}
onClick={() => {
handlerChangeRelic(selectedRelicSlot)
}}
className="btn btn-info"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button>
</div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div>
</div>
{/* Right Section - Stats and Set Effects */}
<div className="space-y-6">
{/* Set Effects Panel */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("setEffects")}
</h3>
<div className="space-y-6">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="space-y-3">
<div className="flex items-center gap-2">
<div
className="font-bold text-warning"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div>
<div className="space-y-2 pl-4">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
</motion.button>
</div>
{title && (
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
)}
{content}
</div>
</dialog>
))}
</div>
);
}
+112 -112
View File
@@ -1,112 +1,112 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
+118 -118
View File
@@ -1,118 +1,118 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import { replaceByParam } from '@/helper'
import useLocaleStore from '@/stores/localeStore'
import { themeColors } from '@/constant/constant'
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
id: string
name: string
time?: string
description?: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex flex-col gap-1 w-full h-full">
<div
className="font-bold text-lg"
dangerouslySetInnerHTML={{
__html: replaceByParam(
option.name,
[]
)
}}
/>
{option.time && <div className='text-base'>{option.time}</div>}
{option.description && <div
className="text-base"
dangerouslySetInnerHTML={{
__html: option.description
}}
/>}
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
value={options.find(opt => {
return opt.id === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.id || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import { replaceByParam } from '@/helper'
import useLocaleStore from '@/stores/localeStore'
import { themeColors } from '@/constant/constant'
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
id: string
name: string
time?: string
description?: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex flex-col gap-1 w-full h-full">
<div
className="font-bold text-lg"
dangerouslySetInnerHTML={{
__html: replaceByParam(
option.name,
[]
)
}}
/>
{option.time && <div className='text-base'>{option.time}</div>}
{option.description && <div
className="text-base"
dangerouslySetInnerHTML={{
__html: option.description
}}
/>}
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
value={options.find(opt => {
return opt.id === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.id || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
File diff suppressed because it is too large Load Diff
+107 -107
View File
@@ -1,108 +1,108 @@
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<>
<div
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={78}
height={78}
alt="Relic Icon"
className="h-19.5 w-19.5 rounded-lg"
/>
<div
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{
left: '0.65rem',
bottom: '-0.45rem',
fontSize: '1.05rem',
letterSpacing: '-0.1em',
}}
>
</div>
</div>
<div className=" flex w-1/6 flex-col items-center justify-center">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={35}
height={35}
alt="Main Affix Icon"
className="h-8.75 w-8.75"
/>
</div>
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
alt="Sub Affix Icon"
className="h-6 w-6 shrink-0"
/>
) : (
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
<span className="text-xs text-white/50">?</span>
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span>
{
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
{subAffix?.count}
</span>
)}
</div>
</div>
)
})}
</div>
</div>
</>
)
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<>
<div
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={78}
height={78}
alt="Relic Icon"
className="h-19.5 w-19.5 rounded-lg"
/>
<div
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{
left: '0.65rem',
bottom: '-0.45rem',
fontSize: '1.05rem',
letterSpacing: '-0.1em',
}}
>
</div>
</div>
<div className=" flex w-1/6 flex-col items-center justify-center">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={35}
height={35}
alt="Main Affix Icon"
className="h-8.75 w-8.75"
/>
</div>
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
alt="Sub Affix Icon"
className="h-6 w-6 shrink-0"
/>
) : (
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
<span className="text-xs text-white/50">?</span>
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span>
{
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
{subAffix?.count}
</span>
)}
</div>
</div>
)
})}
</div>
</div>
</>
)
}
+384 -384
View File
@@ -1,385 +1,385 @@
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme } = useLocaleStore()
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
const { avatars, setAvatar } = useUserDataStore()
const { locale } = useLocaleStore()
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("skills")}
</h2>
<div className="flex flex-col items-center">
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
{traceButtons && avatarSelected && (
<div className="grid col-span-4 relative w-full aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt=""
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className={`w-full h-full object-cover rounded-xl`}
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null
}
return (
<div
key={`${btn.id} + ${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black
bg-no-repeat bg-contain
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110z-10
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
? "opacity-50 cursor-not-allowed"
: ""}
`}
onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
}}
/>
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
<p className="
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
font-bold text-center rounded-full absolute
translate-y-full mt-1
bg-base-300 px-1
left-1/2 transform -translate-x-1/2
">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
{btn.size === "special" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
borderRadius: "50%"
}}
/>
)}
{btn.size === "memory" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.4,
borderRadius: "50%"
}}
/>
)}
{btn.size === "elation" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
borderRadius: "50%"
}}
/>
)}
{btn.size === "big" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.3,
borderRadius: "50%"
}}
/>
)}
</div>
)
})}
</div>
)}
{!traceButtons && avatarSelected && (
<div className="flex flex-col relative w-full aspect-square">
</div>
)}
</div>
</div>
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("details")}
</h2>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
<div>
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div>
<div className="font-bold text-success">{transI18n("level")}</div>
<div className="w-full max-w-xs">
<input type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-success"
step="1" />
<div className="flex justify-between px-2.5 mt-2 text-xs">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
<span key={index}>{index}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-success"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<div className="font-bold text-success">
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</div>
</div>
) : (
null
)}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
&& (
<div className="text-xl font-bold flex items-center gap-2 mt-2">
{getLocaleName(locale, skillInfo.PointName)}
{skillInfo.StatusAddList.length > 0 && (
<div>
{skillInfo.StatusAddList.map((status, index) => (
<div key={index}>
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
</div>
))}
</div>
)}
</div>
)}
{skillInfo?.PointDesc && (
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{skillInfo?.LevelUpSkillID
&& skillInfo?.LevelUpSkillID.length > 0
&& dataLevelUpSkill
&& (
<div className="mt-2 flex flex-col gap-2">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index}>
<div className="text-xl font-bold text-primary">
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
</div>
<SkillDescription
skill={skill}
locale={locale}
avatarData={avatarData}
skillInfo={skillInfo}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme } = useLocaleStore()
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
const { avatars, setAvatar } = useUserDataStore()
const { locale } = useLocaleStore()
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("skills")}
</h2>
<div className="flex flex-col items-center">
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
{traceButtons && avatarSelected && (
<div className="grid col-span-4 relative w-full aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt=""
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className={`w-full h-full object-cover rounded-xl`}
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null
}
return (
<div
key={`${btn.id} + ${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black
bg-no-repeat bg-contain
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110z-10
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
? "opacity-50 cursor-not-allowed"
: ""}
`}
onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
}}
/>
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
<p className="
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
font-bold text-center rounded-full absolute
translate-y-full mt-1
bg-base-300 px-1
left-1/2 transform -translate-x-1/2
">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
{btn.size === "special" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
borderRadius: "50%"
}}
/>
)}
{btn.size === "memory" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.4,
borderRadius: "50%"
}}
/>
)}
{btn.size === "elation" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
borderRadius: "50%"
}}
/>
)}
{btn.size === "big" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.3,
borderRadius: "50%"
}}
/>
)}
</div>
)
})}
</div>
)}
{!traceButtons && avatarSelected && (
<div className="flex flex-col relative w-full aspect-square">
</div>
)}
</div>
</div>
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("details")}
</h2>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
<div>
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div>
<div className="font-bold text-success">{transI18n("level")}</div>
<div className="w-full max-w-xs">
<input type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-success"
step="1" />
<div className="flex justify-between px-2.5 mt-2 text-xs">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
<span key={index}>{index}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-success"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<div className="font-bold text-success">
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</div>
</div>
) : (
null
)}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
&& (
<div className="text-xl font-bold flex items-center gap-2 mt-2">
{getLocaleName(locale, skillInfo.PointName)}
{skillInfo.StatusAddList.length > 0 && (
<div>
{skillInfo.StatusAddList.map((status, index) => (
<div key={index}>
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
</div>
))}
</div>
)}
</div>
)}
{skillInfo?.PointDesc && (
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{skillInfo?.LevelUpSkillID
&& skillInfo?.LevelUpSkillID.length > 0
&& dataLevelUpSkill
&& (
<div className="mt-2 flex flex-col gap-2">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index}>
<div className="text-xl font-bold text-primary">
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
</div>
<SkillDescription
skill={skill}
locale={locale}
avatarData={avatarData}
skillInfo={skillInfo}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
+40 -40
View File
@@ -1,41 +1,41 @@
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
return (
<div className="flex flex-col gap-2">
<div className="space-y-2 pb-2">
<div className="flex items-center gap-2">
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
<div
className="text-lg font-bold tracking-wide text-foreground uppercase"
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
/>
</div>
<div
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
</div>
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
return (
<div className="flex flex-col gap-2">
<div className="space-y-2 pb-2">
<div className="flex items-center gap-2">
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
<div
className="text-lg font-bold tracking-wide text-foreground uppercase"
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
/>
</div>
<div
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
</div>
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
};
@@ -1,8 +1,8 @@
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./clientThemeWrapper"
export * from "./themeContext"
export * from "./clientThemeWrapper"
export * from "./themeContext"
+40 -40
View File
@@ -1,41 +1,41 @@
"use client";
import { createContext, PropsWithChildren, useEffect } from "react";
import useLocaleStore from "@/stores/localeStore";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore()
useEffect(() => {
if (typeof window !== "undefined") {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) setTheme(storedTheme);
}
}, [setTheme]);
const changeTheme = (nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme);
}
} else {
setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
}
};
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
"use client";
import { createContext, PropsWithChildren, useEffect } from "react";
import useLocaleStore from "@/stores/localeStore";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore()
useEffect(() => {
if (typeof window !== "undefined") {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) setTheme(storedTheme);
}
}, [setTheme]);
const changeTheme = (nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme);
}
} else {
setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
}
};
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
};
+220 -220
View File
@@ -1,220 +1,220 @@
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
+1147 -1147
View File
File diff suppressed because it is too large Load Diff
+104 -104
View File
@@ -1,105 +1,105 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
}
+14 -14
View File
@@ -1,15 +1,15 @@
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}
+119 -119
View File
@@ -1,89 +1,89 @@
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
Object.entries(avatars ?? {}).forEach(([avatarId, avatar]) => {
if (!avatar) return
@@ -91,10 +91,10 @@ export function converterToFreeSRJson(
for (const [skillId, level] of Object.entries(avatar?.data?.skills || {})) {
if (skillConfig?.[skillId]) {
skillsByAnchorType[skillConfig[skillId].IndexSlot] = level > skillConfig[skillId].MaxLevel ? skillConfig[skillId].MaxLevel : level
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0),
data: {
rank: Number(avatar.data?.rank || 0),
@@ -115,21 +115,21 @@ export function converterToFreeSRJson(
const newLightcone: LightconeJson = {
level: Number(currentProfile.lightcone.level || 0),
item_id: Number(currentProfile.lightcone.item_id || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
relic_id: Number(relic.relic_id || 0),
relic_set_id: Number(relic.relic_set_id || 0),
main_affix_id: Number(relic.main_affix_id || 0),
@@ -137,18 +137,18 @@ export function converterToFreeSRJson(
internal_uid: internalUidRelic,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
}
+61 -61
View File
@@ -1,62 +1,62 @@
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")}${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")}${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
}
+19 -19
View File
@@ -1,23 +1,23 @@
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
return Object.values(avatarSelected?.SkillTrees ?? {}).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>);
}
}
return acc;
}, {} as Record<string, number>);
}
+7 -7
View File
@@ -1,8 +1,8 @@
export * from "./getName"
export * from "./replaceByParam"
export * from "./converterToAvatarStore"
export * from "./calcData"
export * from "./random"
export * from "./json"
export * from "./convertData"
export * from "./getName"
export * from "./replaceByParam"
export * from "./converterToAvatarStore"
export * from "./calcData"
export * from "./random"
export * from "./json"
export * from "./convertData"
export * from "./connect"
+23 -23
View File
@@ -8,27 +8,27 @@ export function randomPartition(sum: number, parts: number): number[] {
const raw = Array.from({ length: parts }, () => Math.random());
const total = raw.reduce((a, b) => a + b, 0);
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
}
+34 -34
View File
@@ -1,39 +1,39 @@
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
export function replaceByParam(desc?: string, params: number[] = []): string {
if (!desc) return "";
const PARAM_REGEX = /#(\d+)\[(f(\d+)|i)\](%)?/g;
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
}
+1 -1
View File
@@ -1 +1 @@
export * from "./useChangeTheme";
export * from "./useChangeTheme";
+3 -3
View File
@@ -1,4 +1,4 @@
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
export const useChangeTheme = () => useContext(ThemeContext);
+141 -141
View File
@@ -1,142 +1,142 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: any }) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: any }) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
}
+37 -37
View File
@@ -1,38 +1,38 @@
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
}
+9 -9
View File
@@ -1,10 +1,10 @@
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchChangelog";
+26 -26
View File
@@ -1,26 +1,26 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore()
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore()
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
+42 -42
View File
@@ -1,43 +1,43 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected, setAvatarSelected } = useCurrentDataStore()
const query = useQuery({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
for (const avatar of Object.values(avatarStore)) {
setAvatar(avatar)
}
}, [mapAvatar])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected, setAvatarSelected } = useCurrentDataStore()
const query = useQuery({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
for (const avatar of Object.values(avatarStore)) {
setAvatar(avatar)
}
}, [mapAvatar])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
}
+32 -32
View File
@@ -1,32 +1,32 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
const { setIsChangelog } = useModelStore()
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
const { setIsChangelog } = useModelStore()
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore()
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore()
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore()
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore()
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
+33 -33
View File
@@ -1,33 +1,33 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore()
const { setSettingData } = useCurrentDataStore()
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore()
const { setSettingData } = useCurrentDataStore()
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore()
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore()
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore()
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore()
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore()
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore()
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
+25 -25
View File
@@ -1,25 +1,25 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore()
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore()
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}
+24 -24
View File
@@ -2,19 +2,19 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware';
import { connectPersistedSchema } from '@/zod';
import { createValidatedJSONStorage } from './persistStorage';
interface ConnectState {
connectionType: string;
privateType: string;
serverUrl: string;
username: string;
password: string;
setConnectionType: (newConnectionType: string) => void;
setPrivateType: (newPrivateType: string) => void;
setServerUrl: (newServerUrl: string) => void;
setUsername: (newUsername: string) => void;
setPassword: (newPassword: string) => void;
connectionType: string;
privateType: string;
serverUrl: string;
username: string;
password: string;
setConnectionType: (newConnectionType: string) => void;
setPrivateType: (newPrivateType: string) => void;
setServerUrl: (newServerUrl: string) => void;
setUsername: (newUsername: string) => void;
setPassword: (newPassword: string) => void;
}
type ConnectPersistedState = Pick<ConnectState, "connectionType" | "privateType" | "serverUrl" | "username" | "password">;
@@ -22,17 +22,17 @@ type ConnectPersistedState = Pick<ConnectState, "connectionType" | "privateType"
const useConnectStore = create<ConnectState>()(
persist<ConnectState, [], [], ConnectPersistedState>(
(set) => ({
connectionType: "FireflyGo",
privateType: "Local",
serverUrl: "http://localhost:21000",
username: "",
password: "",
setConnectionType: (newConnectionType: string) => set({ connectionType: newConnectionType }),
setPrivateType: (newPrivateType: string) => set({ privateType: newPrivateType }),
setServerUrl: (newServerUrl: string) => set({ serverUrl: newServerUrl }),
setUsername: (newUsername: string) => set({ username: newUsername }),
setPassword: (newPassword: string) => set({ password: newPassword }),
}),
connectionType: "FireflyGo",
privateType: "Local",
serverUrl: "http://localhost:21000",
username: "",
password: "",
setConnectionType: (newConnectionType: string) => set({ connectionType: newConnectionType }),
setPrivateType: (newPrivateType: string) => set({ privateType: newPrivateType }),
setServerUrl: (newServerUrl: string) => set({ serverUrl: newServerUrl }),
setUsername: (newUsername: string) => set({ username: newUsername }),
setPassword: (newPassword: string) => set({ password: newPassword }),
}),
{
name: 'connect-storage',
storage: createValidatedJSONStorage(() => localStorage, connectPersistedSchema),
@@ -46,5 +46,5 @@ const useConnectStore = create<ConnectState>()(
}
)
);
export default useConnectStore;
+40 -40
View File
@@ -1,41 +1,41 @@
import { AvatarDetail, AvatarProfileCardType, BaseTypeData, DamageTypeData } from '@/types';
import { create } from 'zustand'
interface CopyProfileState {
selectedProfiles: AvatarProfileCardType[];
avatarCopySelected: AvatarDetail | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => void;
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => void;
}
const useCopyProfileStore = create<CopyProfileState>((set) => ({
selectedProfiles: [],
avatarCopySelected: null,
listElement: {},
listPath: {},
listRank: { "4": false, "5": false },
listCopyAvatar: [],
listRawCopyAvatar: [],
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => set({ avatarCopySelected: newAvatarSelected }),
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => set({ selectedProfiles: newListAvatar }),
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => {
set({
listElement: Object.fromEntries(Object.keys(damageType).map(key => [key, false])) as Record<string, boolean>,
listPath: Object.fromEntries(Object.keys(baseType).map(key => [key, false])) as Record<string, boolean>,
listRank: { "4": false, "5": false }
})
}
}));
import { AvatarDetail, AvatarProfileCardType, BaseTypeData, DamageTypeData } from '@/types';
import { create } from 'zustand'
interface CopyProfileState {
selectedProfiles: AvatarProfileCardType[];
avatarCopySelected: AvatarDetail | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => void;
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => void;
}
const useCopyProfileStore = create<CopyProfileState>((set) => ({
selectedProfiles: [],
avatarCopySelected: null,
listElement: {},
listPath: {},
listRank: { "4": false, "5": false },
listCopyAvatar: [],
listRawCopyAvatar: [],
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => set({ avatarCopySelected: newAvatarSelected }),
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => set({ selectedProfiles: newListAvatar }),
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => {
set({
listElement: Object.fromEntries(Object.keys(damageType).map(key => [key, false])) as Record<string, boolean>,
listPath: Object.fromEntries(Object.keys(baseType).map(key => [key, false])) as Record<string, boolean>,
listRank: { "4": false, "5": false }
})
}
}));
export default useCopyProfileStore;

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