Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6be379702 | ||
|
|
5e3557e2da | ||
|
|
a475b1b554 | ||
|
|
dc5f1c1370 | ||
|
|
8e34d08545 | ||
|
|
65a143bec2 | ||
|
|
9a52bac7f1 | ||
|
|
c16d264209 | ||
|
|
9469486174 | ||
|
|
ba3330c0f3 | ||
|
|
859418e50c | ||
|
|
3fe1e4a347 | ||
|
|
d7dfe3c824 | ||
|
|
18843e271f | ||
|
|
1185dad4da | ||
|
|
5c4ca2b07c | ||
|
|
1368bb4445 |
89
.github/workflows/build.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.23'
|
||||||
|
|
||||||
- name: Build Frontend & Server
|
- name: Build Frontend & Server
|
||||||
run: |
|
run: |
|
||||||
@@ -49,7 +49,71 @@ jobs:
|
|||||||
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
|
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
path: ${{ matrix.executable }}
|
path: ${{ matrix.executable }}
|
||||||
|
|
||||||
# 2. 构建 Electron 客户端
|
# 2. 构建 Android APK (ARM64, ARMv7, x86_64)
|
||||||
|
build-android:
|
||||||
|
name: Build Android APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Build Frontend
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
- name: Generate Android Icons
|
||||||
|
run: |
|
||||||
|
# 安装 ImageMagick
|
||||||
|
sudo apt-get update && sudo apt-get install -y imagemagick
|
||||||
|
# 使用 ImageMagick 生成各尺寸图标
|
||||||
|
convert logo.png -resize 48x48 android/app/src/main/res/mipmap-mdpi/ic_launcher.png
|
||||||
|
convert logo.png -resize 48x48 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
|
||||||
|
convert logo.png -resize 72x72 android/app/src/main/res/mipmap-hdpi/ic_launcher.png
|
||||||
|
convert logo.png -resize 72x72 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
|
||||||
|
convert logo.png -resize 96x96 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
|
||||||
|
convert logo.png -resize 96x96 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
|
||||||
|
convert logo.png -resize 144x144 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
|
||||||
|
convert logo.png -resize 144x144 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
|
||||||
|
convert logo.png -resize 192x192 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
|
||||||
|
convert logo.png -resize 192x192 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
|
||||||
|
# foreground 图标需要更大(108dp with 72dp safe zone)
|
||||||
|
convert logo.png -resize 108x108 android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
|
||||||
|
convert logo.png -resize 162x162 android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
|
||||||
|
convert logo.png -resize 216x216 android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
|
||||||
|
convert logo.png -resize 324x324 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
|
||||||
|
convert logo.png -resize 432x432 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
|
||||||
|
|
||||||
|
- name: Sync Capacitor
|
||||||
|
run: npx cap sync android
|
||||||
|
|
||||||
|
- name: Build APK (Release)
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
- name: Upload APK Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android-apk
|
||||||
|
path: android/app/build/outputs/apk/release/*.APK
|
||||||
|
|
||||||
|
# 3. 构建 Electron 客户端
|
||||||
build-electron:
|
build-electron:
|
||||||
name: Build Electron (${{ matrix.os }})
|
name: Build Electron (${{ matrix.os }})
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -91,9 +155,9 @@ jobs:
|
|||||||
pkg/*.dmg
|
pkg/*.dmg
|
||||||
pkg/*.AppImage
|
pkg/*.AppImage
|
||||||
|
|
||||||
# 3. 发布 Release
|
# 4. 发布 Release
|
||||||
release:
|
release:
|
||||||
needs: [ build-server, build-electron ]
|
needs: [ build-server, build-android, build-electron ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -124,11 +188,26 @@ jobs:
|
|||||||
# 移动 Electron 安装包 (排除 unpacked 目录)
|
# 移动 Electron 安装包 (排除 unpacked 目录)
|
||||||
find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \;
|
find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \;
|
||||||
|
|
||||||
|
# 移动 Android APK
|
||||||
|
find ./raw-assets/android-apk -type f -name "*.APK" -exec cp {} ./final-release/ \; || true
|
||||||
|
|
||||||
# 检查结果
|
# 检查结果
|
||||||
echo "Final assets to upload:"
|
echo "Final assets to upload:"
|
||||||
ls -R ./final-release
|
ls -R ./final-release
|
||||||
|
|
||||||
# 3. 一次性上传,禁止重复匹配
|
# 3. 删除已存在的同名 APK 资源(避免 422 错误)
|
||||||
|
- name: Delete existing APK assets
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||||
|
# 获取 release 中的现有 assets 并删除 APK 文件
|
||||||
|
gh release view "$VERSION" --json assets -q '.assets[].name' 2>/dev/null | grep -i '\.APK$' | while read asset; do
|
||||||
|
echo "Deleting existing asset: $asset"
|
||||||
|
gh release delete-asset "$VERSION" "$asset" -y || true
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# 4. 一次性上传,禁止重复匹配
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
9
.gitignore
vendored
@@ -28,3 +28,12 @@ docs
|
|||||||
*.sw?
|
*.sw?
|
||||||
/docs
|
/docs
|
||||||
/docs/assets
|
/docs/assets
|
||||||
|
|
||||||
|
# Android build outputs
|
||||||
|
android/.gradle
|
||||||
|
android/app/build
|
||||||
|
android/build
|
||||||
|
android/local.properties
|
||||||
|
android/.idea
|
||||||
|
android/*.iml
|
||||||
|
android/app/*.iml
|
||||||
|
|||||||
184
README-DE.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
|
||||||
|
|
||||||
|
# OGame Vue TS
|
||||||
|
|
||||||
|
Ein modernes Weltraum-Strategiespiel basierend auf dem klassischen OGame, entwickelt mit Vue 3 und TypeScript.
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
|
[](https://vuejs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://vitejs.dev/)
|
||||||
|
[](https://golang.org/)
|
||||||
|
|
||||||
|
**Downloads:**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README-EN.md) | [繁體中文](README-zh-TW.md) | Deutsch | [Русский](README-RU.md) | [한국어](README-KO.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Über das Projekt
|
||||||
|
|
||||||
|
OGame Vue TS ist ein Einzelspieler-Weltraum-Strategiespiel im Browser, inspiriert vom klassischen OGame. Baue dein Imperium in der Galaxie auf, erforsche Technologien, konstruiere Raumschiffe und nimm an epischen Weltraumschlachten teil. Dieses Projekt wurde mit modernen Webtechnologien entwickelt und läuft vollständig im Browser mit lokaler Datenspeicherung.
|
||||||
|
|
||||||
|
## Hauptfunktionen
|
||||||
|
|
||||||
|
- **Mehrsprachige Unterstützung** - Verfügbar in 6 Sprachen: Englisch, Chinesisch (vereinfacht & traditionell), Deutsch, Russisch und Koreanisch
|
||||||
|
- **Gebäudeverwaltung** - Baue und verbessere verschiedene Gebäude auf Planeten und Monden
|
||||||
|
- **Technologieforschung** - Schalte fortschrittliche Technologien frei, um dein Imperium zu stärken
|
||||||
|
- **Flottenverwaltung** - Baue Schiffe, sende Missionen und nimm an taktischen Weltraumkämpfen teil
|
||||||
|
- **Verteidigungssysteme** - Errichte Verteidigungsanlagen zum Schutz deiner Kolonien
|
||||||
|
- **Offiziersystem** - Rekrutiere Offiziere für strategische Vorteile
|
||||||
|
- **Kampfsimulator** - Teste Kampfszenarien, bevor du Ressourcen einsetzt
|
||||||
|
- **Galaxieansicht** - Erkunde das Universum und interagiere mit anderen Planeten
|
||||||
|
- **Lokale Datenspeicherung** - Alle Spieldaten werden verschlüsselt im Browser gespeichert
|
||||||
|
- **Dunkler/Heller Modus** - Wähle dein bevorzugtes visuelles Thema
|
||||||
|
- **Warteschlangenverwaltung** - Verwalte mehrere Bau- und Forschungswarteschlangen
|
||||||
|
- **Mondgenerierung** - Wahrscheinlichkeitsbasierte Monderzeugung aus Trümmerfeldern
|
||||||
|
|
||||||
|
## Technologie-Stack
|
||||||
|
|
||||||
|
- **Frontend-Framework:** [Vue 3](https://vuejs.org/) + Composition API (`<script setup>` Syntax)
|
||||||
|
- **Programmiersprache:** [TypeScript](https://www.typescriptlang.org/) (mit strikter Typprüfung)
|
||||||
|
- **Build-Tool:** [Vite](https://vitejs.dev/) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org/) (für plattformübergreifenden Webserver), [Electron](https://www.electronjs.org/) (für plattformübergreifende Desktop-Anwendung)
|
||||||
|
- **Zustandsverwaltung:** [Pinia](https://pinia.vuejs.org/) + Persistenz-Plugin
|
||||||
|
- **Routing:** [Vue Router 4](https://router.vuejs.org/)
|
||||||
|
- **UI-Komponenten:** [shadcn-vue](https://www.shadcn-vue.com/) (New York Stil)
|
||||||
|
- **Styling:** [Tailwind CSS v4](https://tailwindcss.com/) + CSS-Variablen
|
||||||
|
- **Icons:** [Lucide Vue Next](https://lucide.dev/)
|
||||||
|
- **Animationen:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
|
||||||
|
- **Internationalisierung:** Eigene i18n-Implementierung
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (Version 18 oder höher empfohlen)
|
||||||
|
- [pnpm](https://pnpm.io/) (Version 10.13.1 oder höher)
|
||||||
|
- [Go](https://golang.org/) (Version 1.21 oder höher) (optional)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Repository klonen
|
||||||
|
git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
|
|
||||||
|
# In das Projektverzeichnis wechseln
|
||||||
|
cd ogame-vue-ts
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entwicklung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Entwicklungsserver starten (läuft auf Port 25121)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Öffne deinen Browser und besuche `http://localhost:25121`
|
||||||
|
|
||||||
|
### Produktions-Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Anwendung bauen
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Produktions-Build vorschauen
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verfügbare Sprachen
|
||||||
|
|
||||||
|
- 简体中文 (Vereinfachtes Chinesisch)
|
||||||
|
- 繁體中文 (Traditionelles Chinesisch)
|
||||||
|
- English (Englisch)
|
||||||
|
- Deutsch
|
||||||
|
- Русский (Russisch)
|
||||||
|
- 한국어 (Koreanisch)
|
||||||
|
|
||||||
|
## Spielfunktionen
|
||||||
|
|
||||||
|
### Ressourcenverwaltung
|
||||||
|
- **Metall** - Primäres Baumaterial
|
||||||
|
- **Kristall** - Fortschrittliche Technologiekomponente
|
||||||
|
- **Deuterium** - Treibstoff und Forschungsressource
|
||||||
|
- **Dunkle Materie** - Premium-Ressource
|
||||||
|
- **Energie** - Versorgt deine Anlagen
|
||||||
|
|
||||||
|
### Gebäudetypen
|
||||||
|
- **Ressourcengebäude** - Metallmine, Kristallmine, Deuterium-Synthesizer, Solarkraftwerk
|
||||||
|
- **Anlagen** - Roboterfabrik, Raumschiffwerft, Forschungslabor, Lagereinrichtungen
|
||||||
|
- **Spezialgebäude** - Nanitenfabrik, Terraformer und mehr
|
||||||
|
|
||||||
|
### Technologien
|
||||||
|
- **Energietechnik** - Verbessert die Energieeffizienz
|
||||||
|
- **Lasertechnik** - Verbessert Waffensysteme
|
||||||
|
- **Ionentechnik** - Fortschrittlicher Antrieb und Waffen
|
||||||
|
- **Hyperraumtechnik** - Ermöglicht schnelleres Reisen
|
||||||
|
- **Plasmatechnik** - Ultimative Waffensysteme
|
||||||
|
- Und vieles mehr...
|
||||||
|
|
||||||
|
### Schiffsklassen
|
||||||
|
- **Zivilschiffe** - Kleiner/Großer Transporter, Kolonieschiff, Recycler
|
||||||
|
- **Kampfschiffe** - Leichter/Schwerer Jäger, Kreuzer, Schlachtschiff, Bomber
|
||||||
|
- **Spezialschiffe** - Todesstern, Schlachtkreuzer, Zerstörer
|
||||||
|
|
||||||
|
### Verteidigungssysteme
|
||||||
|
- Raketenwerfer, Leichtes/Schweres Lasergeschütz, Gaußkanone
|
||||||
|
- Ionengeschütz, Plasmawerfer
|
||||||
|
- Kleine/Große Schildkuppel
|
||||||
|
|
||||||
|
## Datensicherheit
|
||||||
|
|
||||||
|
Alle Spieldaten werden automatisch mit AES-Verschlüsselung verschlüsselt, bevor sie im lokalen Speicher des Browsers gespeichert werden. Dein Spielfortschritt ist sicher und privat.
|
||||||
|
|
||||||
|
## Anpassung
|
||||||
|
|
||||||
|
Die Anwendung unterstützt vollständige Theme-Anpassung durch Tailwind CSS-Variablen, die in `src/style.css` definiert sind. Du kannst einfach zwischen hellem und dunklem Modus wechseln.
|
||||||
|
|
||||||
|
## Mitwirken
|
||||||
|
|
||||||
|
Beiträge sind willkommen! Bitte zögere nicht, Issues oder Pull Requests einzureichen.
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
Dieses Werk ist lizenziert unter der [Creative Commons Namensnennung-Nicht kommerziell 4.0 International Lizenz](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||||
|
|
||||||
|
### Du darfst:
|
||||||
|
- **Teilen** — das Material in jedwedem Format oder Medium vervielfältigen und weiterverbreiten
|
||||||
|
- **Bearbeiten** — das Material remixen, verändern und darauf aufbauen
|
||||||
|
|
||||||
|
### Unter folgenden Bedingungen:
|
||||||
|
- **Namensnennung** — Du musst angemessene Urheber- und Rechteangaben machen, einen Link zur Lizenz beifügen und angeben, ob Änderungen vorgenommen wurden
|
||||||
|
- **Nicht kommerziell** — Du darfst das Material nicht für kommerzielle Zwecke nutzen
|
||||||
|
|
||||||
|
## Danksagung
|
||||||
|
|
||||||
|
Dieses Projekt wurde vom originalen [OGame](https://ogame.org/) Browserspiel inspiriert. Alle Spielmechaniken und Designelemente wurden zu Bildungs- und Unterhaltungszwecken neu implementiert.
|
||||||
|
|
||||||
|
## Haftungsausschluss
|
||||||
|
|
||||||
|
Dieses Projekt ist nicht mit Gameforge AG oder dem offiziellen OGame-Spiel verbunden, wird nicht von diesen unterstützt oder ist mit diesen verbunden. Es ist ein unabhängiges Fan-Projekt, das zu Bildungszwecken und zur persönlichen Unterhaltung erstellt wurde.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
Mit ❤️ erstellt von <a href="https://github.com/setube">setube</a>
|
||||||
|
<br>
|
||||||
|
© 2025 - Alle Rechte vorbehalten (außer den durch die CC BY-NC 4.0 Lizenz gewährten Rechten)
|
||||||
|
</div>
|
||||||
106
README-EN.md
@@ -5,13 +5,28 @@
|
|||||||
|
|
||||||
A modern of the classic OGame space strategy game, built with Vue 3 and TypeScript.
|
A modern of the classic OGame space strategy game, built with Vue 3 and TypeScript.
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://vitejs.dev/)
|
[](https://vitejs.dev/)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
|
|
||||||
[简体中文](README.md) | English
|
**Downloads:**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[简体中文](README.md) | English | [繁體中文](README-zh-TW.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [한국어](README-KO.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,28 +64,6 @@ OGame Vue TS is a single-player, browser-based space strategy game inspired by t
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Download Build Product
|
|
||||||
|
|
||||||
#### Server version
|
|
||||||
|
|
||||||
[Windows](/releases/latest/download/ogame-windows-amd64.exe)
|
|
||||||
|
|
||||||
[Linux amd64](/releases/latest/download/ogame-linux-amd64)
|
|
||||||
|
|
||||||
[Linux arm64](/releases/latest/download/ogame-linux-arm64)
|
|
||||||
|
|
||||||
[MacOS Intel](/releases/latest/download/ogame-macos-amd64)
|
|
||||||
|
|
||||||
[MacOS](/releases/latest/download/ogame-macos-arm64)
|
|
||||||
|
|
||||||
#### Desktop version
|
|
||||||
|
|
||||||
[Windows](/releases/latest/download/OGame.Setup.exe)
|
|
||||||
|
|
||||||
[Ubuntu](/releases/latest/download/OGame.AppImage)
|
|
||||||
|
|
||||||
[MacOS](/releases/latest/download/OGame-mac.dmg)
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (version 18 or higher recommended)
|
- [Node.js](https://nodejs.org/) (version 18 or higher recommended)
|
||||||
@@ -109,65 +102,14 @@ pnpm build
|
|||||||
pnpm preview
|
pnpm preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ogame-vue-ts/
|
|
||||||
├── public/ # Static assets
|
|
||||||
│ └── logo.svg # Application logo
|
|
||||||
├── src/
|
|
||||||
│ ├── assets/ # Dynamic assets
|
|
||||||
│ ├── components/ # Vue components
|
|
||||||
│ │ └── ui/ # shadcn-vue UI components
|
|
||||||
│ ├── composables/ # Vue composables
|
|
||||||
│ ├── config/ # Game configuration
|
|
||||||
│ ├── lib/ # Utility libraries
|
|
||||||
│ ├── locales/ # i18n translation files
|
|
||||||
│ ├── logic/ # Game logic modules
|
|
||||||
│ │ ├── buildingLogic.ts
|
|
||||||
│ │ ├── buildingValidation.ts
|
|
||||||
│ │ ├── fleetLogic.ts
|
|
||||||
│ │ ├── moonLogic.ts
|
|
||||||
│ │ ├── moonValidation.ts
|
|
||||||
│ │ ├── researchLogic.ts
|
|
||||||
│ │ ├── researchValidation.ts
|
|
||||||
│ │ ├── shipLogic.ts
|
|
||||||
│ │ └── shipValidation.ts
|
|
||||||
│ ├── router/ # Vue Router configuration
|
|
||||||
│ ├── stores/ # Pinia state stores
|
|
||||||
│ ├── types/ # TypeScript type definitions
|
|
||||||
│ ├── utils/ # Utility functions
|
|
||||||
│ ├── views/ # Page components
|
|
||||||
│ │ ├── OverviewView.vue
|
|
||||||
│ │ ├── BuildingsView.vue
|
|
||||||
│ │ ├── ResearchView.vue
|
|
||||||
│ │ ├── ShipyardView.vue
|
|
||||||
│ │ ├── DefenseView.vue
|
|
||||||
│ │ ├── FleetView.vue
|
|
||||||
│ │ ├── GalaxyView.vue
|
|
||||||
│ │ ├── OfficersView.vue
|
|
||||||
│ │ ├── BattleSimulatorView.vue
|
|
||||||
│ │ ├── MessagesView.vue
|
|
||||||
│ │ └── SettingsView.vue
|
|
||||||
│ ├── App.vue # Root component
|
|
||||||
│ ├── main.ts # Application entry point
|
|
||||||
│ └── style.css # Global styles
|
|
||||||
├── .github/
|
|
||||||
│ └── ISSUE_TEMPLATE/ # GitHub issue templates
|
|
||||||
├── LICENSE # CC BY-NC 4.0 License
|
|
||||||
├── package.json # Project dependencies
|
|
||||||
├── tsconfig.json # TypeScript configuration
|
|
||||||
└── vite.config.ts # Vite configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Languages
|
## Available Languages
|
||||||
|
|
||||||
- 🇺🇸 English
|
- 简体中文 (Simplified Chinese)
|
||||||
- 🇨🇳 简体中文 (Simplified Chinese)
|
- 繁體中文 (Traditional Chinese)
|
||||||
- 🇹🇼 繁體中文 (Traditional Chinese)
|
- English
|
||||||
- 🇩🇪 Deutsch (German)
|
- Deutsch (German)
|
||||||
- 🇷🇺 Русский (Russian)
|
- Русский (Russian)
|
||||||
- 🇰🇷 한국어 (Korean)
|
- 한국어 (Korean)
|
||||||
|
|
||||||
## Game Features
|
## Game Features
|
||||||
|
|
||||||
@@ -238,7 +180,7 @@ This project is not affiliated with, endorsed by, or connected to Gameforge AG o
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
Made with ❤️ by Jun Qian
|
Made with ❤️ by <a href="https://github.com/setube">setube</a>
|
||||||
<br>
|
<br>
|
||||||
© 2025 - All rights reserved (except those granted by CC BY-NC 4.0 License)
|
© 2025 - All rights reserved (except those granted by CC BY-NC 4.0 License)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
184
README-KO.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
|
||||||
|
|
||||||
|
# OGame Vue TS
|
||||||
|
|
||||||
|
Vue 3와 TypeScript로 제작된 클래식 OGame을 기반으로 한 현대적인 우주 전략 게임입니다.
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
|
[](https://vuejs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://vitejs.dev/)
|
||||||
|
[](https://golang.org/)
|
||||||
|
|
||||||
|
**다운로드:**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README-EN.md) | [繁體中文](README-zh-TW.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | 한국어
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 프로젝트 소개
|
||||||
|
|
||||||
|
OGame Vue TS는 클래식 OGame에서 영감을 받은 싱글플레이어 브라우저 기반 우주 전략 게임입니다. 은하계에서 제국을 건설하고, 기술을 연구하고, 우주선을 제작하고, 장대한 우주 전투에 참여하세요. 이 프로젝트는 현대 웹 기술로 제작되었으며, 로컬 데이터 저장과 함께 브라우저에서 완전히 실행됩니다.
|
||||||
|
|
||||||
|
## 주요 기능
|
||||||
|
|
||||||
|
- **다국어 지원** - 6개 언어 지원: 영어, 중국어 (간체 및 번체), 독일어, 러시아어, 한국어
|
||||||
|
- **건물 관리** - 행성과 달에서 다양한 건물을 건설하고 업그레이드
|
||||||
|
- **기술 연구** - 제국을 강화하기 위한 첨단 기술 해금
|
||||||
|
- **함대 관리** - 우주선 건조, 미션 파견, 전술적 우주 전투 참여
|
||||||
|
- **방어 시스템** - 식민지 보호를 위한 방어 시설 배치
|
||||||
|
- **장교 시스템** - 전략적 이점을 위한 장교 고용
|
||||||
|
- **전투 시뮬레이터** - 자원 투입 전 전투 시나리오 테스트
|
||||||
|
- **은하 뷰** - 우주 탐험 및 다른 행성과의 상호작용
|
||||||
|
- **로컬 데이터 저장** - 모든 게임 데이터는 암호화되어 브라우저에 로컬 저장
|
||||||
|
- **다크/라이트 모드** - 선호하는 비주얼 테마 선택
|
||||||
|
- **대기열 관리** - 여러 건설 및 연구 대기열 관리
|
||||||
|
- **달 생성** - 잔해 필드에서 확률 기반 달 생성
|
||||||
|
|
||||||
|
## 기술 스택
|
||||||
|
|
||||||
|
- **프론트엔드 프레임워크:** [Vue 3](https://vuejs.org/) + Composition API (`<script setup>` 문법)
|
||||||
|
- **프로그래밍 언어:** [TypeScript](https://www.typescriptlang.org/) (엄격한 타입 검사 활성화)
|
||||||
|
- **빌드 도구:** [Vite](https://vitejs.dev/) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org/) (크로스 플랫폼 웹 서버 구축), [Electron](https://www.electronjs.org/) (크로스 플랫폼 데스크톱 애플리케이션 구축)
|
||||||
|
- **상태 관리:** [Pinia](https://pinia.vuejs.org/) + 지속성 플러그인
|
||||||
|
- **라우팅:** [Vue Router 4](https://router.vuejs.org/)
|
||||||
|
- **UI 컴포넌트:** [shadcn-vue](https://www.shadcn-vue.com/) (New York 스타일)
|
||||||
|
- **스타일링:** [Tailwind CSS v4](https://tailwindcss.com/) + CSS 변수
|
||||||
|
- **아이콘:** [Lucide Vue Next](https://lucide.dev/)
|
||||||
|
- **애니메이션:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
|
||||||
|
- **국제화:** 커스텀 i18n 구현
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### 요구 사항
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (버전 18 이상 권장)
|
||||||
|
- [pnpm](https://pnpm.io/) (버전 10.13.1 이상)
|
||||||
|
- [Go](https://golang.org/) (버전 1.21 이상) (선택 사항)
|
||||||
|
|
||||||
|
### 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 저장소 클론
|
||||||
|
git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
|
|
||||||
|
# 프로젝트 디렉토리로 이동
|
||||||
|
cd ogame-vue-ts
|
||||||
|
|
||||||
|
# 의존성 설치
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 개발
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 개발 서버 시작 (포트 25121에서 실행)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
브라우저를 열고 `http://localhost:25121`로 이동하세요
|
||||||
|
|
||||||
|
### 프로덕션 빌드
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 애플리케이션 빌드
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 프로덕션 빌드 미리보기
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 지원 언어
|
||||||
|
|
||||||
|
- 简体中文 (중국어 간체)
|
||||||
|
- 繁體中文 (중국어 번체)
|
||||||
|
- English (영어)
|
||||||
|
- Deutsch (독일어)
|
||||||
|
- Русский (러시아어)
|
||||||
|
- 한국어
|
||||||
|
|
||||||
|
## 게임 기능
|
||||||
|
|
||||||
|
### 자원 관리
|
||||||
|
- **금속** - 기본 건설 재료
|
||||||
|
- **크리스탈** - 첨단 기술 부품
|
||||||
|
- **중수소** - 연료 및 연구 자원
|
||||||
|
- **암흑 물질** - 프리미엄 자원
|
||||||
|
- **에너지** - 시설에 전력 공급
|
||||||
|
|
||||||
|
### 건물 유형
|
||||||
|
- **자원 건물** - 금속 광산, 크리스탈 광산, 중수소 합성기, 태양열 발전소
|
||||||
|
- **시설** - 로봇 공장, 조선소, 연구소, 저장 시설
|
||||||
|
- **특수 건물** - 나노 로봇 공장, 테라포머 등
|
||||||
|
|
||||||
|
### 기술
|
||||||
|
- **에너지 기술** - 에너지 효율 향상
|
||||||
|
- **레이저 기술** - 무기 시스템 강화
|
||||||
|
- **이온 기술** - 고급 추진 및 무기
|
||||||
|
- **초공간 기술** - 더 빠른 이동 가능
|
||||||
|
- **플라즈마 기술** - 궁극의 무기 시스템
|
||||||
|
- 그 외 다수...
|
||||||
|
|
||||||
|
### 함선 종류
|
||||||
|
- **민간 함선** - 소형/대형 수송선, 식민선, 재활용선
|
||||||
|
- **전투 함선** - 경/중 전투기, 순양함, 전함, 폭격기
|
||||||
|
- **특수 함선** - 데스스타, 전투순양함, 파괴자
|
||||||
|
|
||||||
|
### 방어 시스템
|
||||||
|
- 로켓 발사대, 경/중 레이저, 가우스 포
|
||||||
|
- 이온 포, 플라즈마 포탑
|
||||||
|
- 소형/대형 실드 돔
|
||||||
|
|
||||||
|
## 데이터 보안
|
||||||
|
|
||||||
|
모든 게임 데이터는 브라우저의 로컬 스토리지에 저장되기 전에 AES 암호화로 자동 암호화됩니다. 게임 진행 상황은 안전하고 비공개로 유지됩니다.
|
||||||
|
|
||||||
|
## 커스터마이징
|
||||||
|
|
||||||
|
애플리케이션은 `src/style.css`에 정의된 Tailwind CSS 변수를 통해 완전한 테마 커스터마이징을 지원합니다. 라이트 모드와 다크 모드 간에 쉽게 전환할 수 있습니다.
|
||||||
|
|
||||||
|
## 기여
|
||||||
|
|
||||||
|
기여를 환영합니다! 이슈나 풀 리퀘스트를 자유롭게 제출해 주세요.
|
||||||
|
|
||||||
|
## 라이선스
|
||||||
|
|
||||||
|
이 작품은 [크리에이티브 커먼즈 저작자표시-비영리 4.0 국제 라이선스](https://creativecommons.org/licenses/by-nc/4.0/)에 따라 라이선스가 부여됩니다.
|
||||||
|
|
||||||
|
### 자유롭게:
|
||||||
|
- **공유** — 어떤 매체나 포맷으로든 자료를 복사하고 재배포할 수 있습니다
|
||||||
|
- **변경** — 자료를 리믹스, 변형하고 자료를 기반으로 2차 저작물을 만들 수 있습니다
|
||||||
|
|
||||||
|
### 다음 조건을 따라야 합니다:
|
||||||
|
- **저작자표시** — 적절한 출처를 표시하고, 라이선스 링크를 제공하며, 변경이 있었는지 표시해야 합니다
|
||||||
|
- **비영리** — 이 자료를 상업적 목적으로 사용할 수 없습니다
|
||||||
|
|
||||||
|
## 감사의 말
|
||||||
|
|
||||||
|
이 프로젝트는 원작 [OGame](https://ogame.org/) 브라우저 게임에서 영감을 받았습니다. 모든 게임 메커니즘과 디자인 요소는 교육 및 오락 목적으로 재구현되었습니다.
|
||||||
|
|
||||||
|
## 면책 조항
|
||||||
|
|
||||||
|
이 프로젝트는 Gameforge AG 또는 공식 OGame 게임과 제휴, 보증 또는 연결되어 있지 않습니다. 이것은 교육 목적과 개인적인 즐거움을 위해 만들어진 독립적인 팬 프로젝트입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
❤️를 담아 제작, 작성자: <a href="https://github.com/setube">setube</a>
|
||||||
|
<br>
|
||||||
|
© 2025 - 모든 권리 보유 (CC BY-NC 4.0 라이선스에 의해 부여된 권리 제외)
|
||||||
|
</div>
|
||||||
184
README-RU.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
|
||||||
|
|
||||||
|
# OGame Vue TS
|
||||||
|
|
||||||
|
Современная космическая стратегическая игра, вдохновлённая классической OGame, созданная на Vue 3 и TypeScript.
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
|
[](https://vuejs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://vitejs.dev/)
|
||||||
|
[](https://golang.org/)
|
||||||
|
|
||||||
|
**Скачать:**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README-EN.md) | [繁體中文](README-zh-TW.md) | [Deutsch](README-DE.md) | Русский | [한국어](README-KO.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## О проекте
|
||||||
|
|
||||||
|
OGame Vue TS — это однопользовательская браузерная космическая стратегическая игра, вдохновлённая классической OGame. Постройте свою империю в галактике, исследуйте технологии, создавайте космические корабли и участвуйте в эпических космических сражениях. Этот проект создан с использованием современных веб-технологий и полностью работает в браузере с локальным хранением данных.
|
||||||
|
|
||||||
|
## Основные возможности
|
||||||
|
|
||||||
|
- **Многоязычная поддержка** - Доступно на 6 языках: английский, китайский (упрощённый и традиционный), немецкий, русский и корейский
|
||||||
|
- **Управление зданиями** - Стройте и улучшайте различные здания на планетах и лунах
|
||||||
|
- **Исследование технологий** - Открывайте передовые технологии для усиления вашей империи
|
||||||
|
- **Управление флотом** - Стройте корабли, отправляйте миссии, участвуйте в тактических космических боях
|
||||||
|
- **Системы обороны** - Размещайте оборонительные сооружения для защиты ваших колоний
|
||||||
|
- **Система офицеров** - Нанимайте офицеров для получения стратегических преимуществ
|
||||||
|
- **Боевой симулятор** - Тестируйте боевые сценарии перед вложением ресурсов
|
||||||
|
- **Вид галактики** - Исследуйте вселенную и взаимодействуйте с другими планетами
|
||||||
|
- **Локальное хранение данных** - Все игровые данные зашифрованы и хранятся локально в браузере
|
||||||
|
- **Тёмный/светлый режим** - Выберите предпочитаемую визуальную тему
|
||||||
|
- **Управление очередями** - Управляйте несколькими очередями строительства и исследований
|
||||||
|
- **Генерация луны** - Вероятностное создание луны из поля обломков
|
||||||
|
|
||||||
|
## Технологический стек
|
||||||
|
|
||||||
|
- **Frontend-фреймворк:** [Vue 3](https://vuejs.org/) + Composition API (синтаксис `<script setup>`)
|
||||||
|
- **Язык программирования:** [TypeScript](https://www.typescriptlang.org/) (со строгой проверкой типов)
|
||||||
|
- **Инструмент сборки:** [Vite](https://vitejs.dev/) (Custom Rolldown-Vite 7.2.5), [Golang](https://golang.org/) (для кроссплатформенного веб-сервера), [Electron](https://www.electronjs.org/) (для кроссплатформенного десктоп-приложения)
|
||||||
|
- **Управление состоянием:** [Pinia](https://pinia.vuejs.org/) + плагин персистентности
|
||||||
|
- **Маршрутизация:** [Vue Router 4](https://router.vuejs.org/)
|
||||||
|
- **UI-компоненты:** [shadcn-vue](https://www.shadcn-vue.com/) (стиль New York)
|
||||||
|
- **Стилизация:** [Tailwind CSS v4](https://tailwindcss.com/) + CSS-переменные
|
||||||
|
- **Иконки:** [Lucide Vue Next](https://lucide.dev/)
|
||||||
|
- **Анимации:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
|
||||||
|
- **Интернационализация:** Собственная реализация i18n
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (рекомендуется версия 18 или выше)
|
||||||
|
- [pnpm](https://pnpm.io/) (версия 10.13.1 или выше)
|
||||||
|
- [Go](https://golang.org/) (версия 1.21 или выше) (опционально)
|
||||||
|
|
||||||
|
### Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Клонировать репозиторий
|
||||||
|
git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
|
|
||||||
|
# Перейти в директорию проекта
|
||||||
|
cd ogame-vue-ts
|
||||||
|
|
||||||
|
# Установить зависимости
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить сервер разработки (работает на порту 25121)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Откройте браузер и перейдите по адресу `http://localhost:25121`
|
||||||
|
|
||||||
|
### Сборка для продакшена
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Собрать приложение
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Предпросмотр продакшен-сборки
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Доступные языки
|
||||||
|
|
||||||
|
- 简体中文 (Упрощённый китайский)
|
||||||
|
- 繁體中文 (Традиционный китайский)
|
||||||
|
- English (Английский)
|
||||||
|
- Deutsch (Немецкий)
|
||||||
|
- Русский
|
||||||
|
- 한국어 (Корейский)
|
||||||
|
|
||||||
|
## Игровые возможности
|
||||||
|
|
||||||
|
### Управление ресурсами
|
||||||
|
- **Металл** - Основной строительный материал
|
||||||
|
- **Кристалл** - Компонент передовых технологий
|
||||||
|
- **Дейтерий** - Топливо и ресурс для исследований
|
||||||
|
- **Тёмная материя** - Премиум-ресурс
|
||||||
|
- **Энергия** - Питает ваши сооружения
|
||||||
|
|
||||||
|
### Типы зданий
|
||||||
|
- **Ресурсные здания** - Металлоплавильня, Кристальный завод, Синтезатор дейтерия, Солнечная электростанция
|
||||||
|
- **Сооружения** - Завод роботов, Верфь, Исследовательская лаборатория, Хранилища
|
||||||
|
- **Специальные здания** - Фабрика нанитов, Терраформер и другие
|
||||||
|
|
||||||
|
### Технологии
|
||||||
|
- **Энергетическая технология** - Повышает энергоэффективность
|
||||||
|
- **Лазерная технология** - Улучшает системы вооружения
|
||||||
|
- **Ионная технология** - Продвинутая двигательная установка и оружие
|
||||||
|
- **Гиперпространственная технология** - Обеспечивает более быстрые перелёты
|
||||||
|
- **Плазменная технология** - Максимальные системы вооружения
|
||||||
|
- И многое другое...
|
||||||
|
|
||||||
|
### Классы кораблей
|
||||||
|
- **Гражданские корабли** - Малый/Большой транспорт, Колонизатор, Переработчик
|
||||||
|
- **Боевые корабли** - Лёгкий/Тяжёлый истребитель, Крейсер, Линкор, Бомбардировщик
|
||||||
|
- **Специальные корабли** - Звезда Смерти, Линейный крейсер, Разрушитель
|
||||||
|
|
||||||
|
### Системы обороны
|
||||||
|
- Ракетная установка, Лёгкий/Тяжёлый лазер, Пушка Гаусса
|
||||||
|
- Ионное орудие, Плазменная турель
|
||||||
|
- Малый/Большой защитный купол
|
||||||
|
|
||||||
|
## Безопасность данных
|
||||||
|
|
||||||
|
Все игровые данные автоматически шифруются с помощью AES перед сохранением в локальном хранилище браузера. Ваш игровой прогресс защищён и приватен.
|
||||||
|
|
||||||
|
## Кастомизация
|
||||||
|
|
||||||
|
Приложение поддерживает полную настройку темы через CSS-переменные Tailwind, определённые в `src/style.css`. Вы можете легко переключаться между светлым и тёмным режимами.
|
||||||
|
|
||||||
|
## Участие в разработке
|
||||||
|
|
||||||
|
Приветствуем вклад в проект! Пожалуйста, не стесняйтесь создавать issues или pull requests.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Эта работа лицензирована под [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||||
|
|
||||||
|
### Вы можете:
|
||||||
|
- **Делиться** — копировать и распространять материал в любом формате или на любом носителе
|
||||||
|
- **Адаптировать** — ремиксовать, преобразовывать и создавать на основе материала
|
||||||
|
|
||||||
|
### При соблюдении следующих условий:
|
||||||
|
- **Атрибуция** — Вы должны указать соответствующую атрибуцию, предоставить ссылку на лицензию и указать, были ли внесены изменения
|
||||||
|
- **Некоммерческое использование** — Вы не можете использовать материал в коммерческих целях
|
||||||
|
|
||||||
|
## Благодарности
|
||||||
|
|
||||||
|
Этот проект вдохновлён оригинальной браузерной игрой [OGame](https://ogame.org/). Все игровые механики и элементы дизайна были переосмыслены в образовательных и развлекательных целях.
|
||||||
|
|
||||||
|
## Отказ от ответственности
|
||||||
|
|
||||||
|
Этот проект не связан с Gameforge AG или официальной игрой OGame, не одобрен и не поддерживается ими. Это независимый фан-проект, созданный в образовательных целях и для личного развлечения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
Сделано с ❤️ автором <a href="https://github.com/setube">setube</a>
|
||||||
|
<br>
|
||||||
|
© 2025 - Все права защищены (кроме прав, предоставленных лицензией CC BY-NC 4.0)
|
||||||
|
</div>
|
||||||
184
README-zh-TW.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="public/logo.svg" alt="OGame Vue TS Logo" width="128" height="128">
|
||||||
|
|
||||||
|
# OGame Vue TS
|
||||||
|
|
||||||
|
一個基於 Vue 3 和 TypeScript 構建的現代化 OGame 太空策略遊戲。
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
|
[](https://vuejs.org/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](https://vitejs.dev/)
|
||||||
|
[](https://golang.org/)
|
||||||
|
|
||||||
|
**下載**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[简体中文](README.md) | [English](README-EN.md) | 繁體中文 | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [한국어](README-KO.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 關於專案
|
||||||
|
|
||||||
|
OGame Vue TS 是一款受經典 OGame 遊戲啟發的單機版、基於瀏覽器的太空策略遊戲。在銀河系中建立你的帝國,研究科技,建造艦船,參與史詩般的太空戰鬥。本專案採用現代 Web 技術構建,完全在瀏覽器中運行,提供流暢且響應迅速的遊戲體驗,所有數據都儲存在本地。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- **多語言支援** - 支援 6 種語言:英語、簡體中文、繁體中文、德語、俄語和韓語
|
||||||
|
- **建築管理** - 在行星和月球上建造和升級各種建築
|
||||||
|
- **科技研究** - 解鎖先進科技來增強你的帝國
|
||||||
|
- **艦隊管理** - 建造艦船、派遣任務、參與戰術太空戰鬥
|
||||||
|
- **防禦系統** - 部署防禦設施來保護你的殖民地
|
||||||
|
- **軍官系統** - 招募軍官以獲得戰略優勢
|
||||||
|
- **戰鬥模擬器** - 在投入資源前測試戰鬥場景
|
||||||
|
- **銀河視圖** - 探索宇宙並與其他星球互動
|
||||||
|
- **本地數據持久化** - 所有遊戲數據都經過加密並儲存在瀏覽器本地
|
||||||
|
- **深色/淺色主題** - 選擇你喜歡的視覺主題
|
||||||
|
- **隊列管理** - 管理多個建造和研究隊列
|
||||||
|
- **月球生成** - 基於概率的月球從殘骸場生成機制
|
||||||
|
|
||||||
|
## 技術棧
|
||||||
|
|
||||||
|
- **前端框架:** [Vue 3](https://vuejs.org/) + Composition API (`<script setup>` 語法)
|
||||||
|
- **開發語言:** [TypeScript](https://www.typescriptlang.org/) (啟用嚴格類型檢查)
|
||||||
|
- **構建工具:** [Vite](https://vitejs.dev/) (自定義 Rolldown-Vite 7.2.5)、[Golang](https://golang.org/)(構建跨平台的Web服務端)、[Electron](https://www.electronjs.org/)(構建跨平台可視化介面)
|
||||||
|
- **狀態管理:** [Pinia](https://pinia.vuejs.org/) + 持久化插件
|
||||||
|
- **路由管理:** [Vue Router 4](https://router.vuejs.org/)
|
||||||
|
- **UI 組件:** [shadcn-vue](https://www.shadcn-vue.com/) (New York 風格)
|
||||||
|
- **樣式方案:** [Tailwind CSS v4](https://tailwindcss.com/) + CSS 變數
|
||||||
|
- **圖標庫:** [Lucide Vue Next](https://lucide.dev/)
|
||||||
|
- **動畫效果:** [tw-animate-css](https://www.npmjs.com/package/tw-animate-css)
|
||||||
|
- **國際化:** 自定義 i18n 實現
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
### 環境要求
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) (推薦 18 或更高版本)
|
||||||
|
- [pnpm](https://pnpm.io/) (版本 10.13.1 或更高)
|
||||||
|
- [Go](https://golang.org/) (版本 1.21 或更高版本)(可選)
|
||||||
|
|
||||||
|
### 安裝
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆倉庫
|
||||||
|
git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
|
|
||||||
|
# 進入專案目錄
|
||||||
|
cd ogame-vue-ts
|
||||||
|
|
||||||
|
# 安裝依賴
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 開發
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 啟動開發服務器 (運行在端口 25121)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
在瀏覽器中訪問 `http://localhost:25121`
|
||||||
|
|
||||||
|
### 生產構建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 構建應用
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 預覽生產構建
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支援的語言
|
||||||
|
|
||||||
|
- 简体中文
|
||||||
|
- 繁體中文
|
||||||
|
- English (英語)
|
||||||
|
- Deutsch (德語)
|
||||||
|
- Русский (俄語)
|
||||||
|
- 한국어 (韓語)
|
||||||
|
|
||||||
|
## 遊戲特性
|
||||||
|
|
||||||
|
### 資源管理
|
||||||
|
- **金屬** - 主要建築材料
|
||||||
|
- **晶體** - 高級科技組件
|
||||||
|
- **重氫** - 燃料和研究資源
|
||||||
|
- **暗物質** - 高級資源
|
||||||
|
- **能量** - 為設施供電
|
||||||
|
|
||||||
|
### 建築類型
|
||||||
|
- **資源建築** - 金屬礦、晶體礦、重氫合成器、太陽能發電廠
|
||||||
|
- **設施建築** - 機器人工廠、船塢、研究實驗室、倉儲設施
|
||||||
|
- **特殊建築** - 納米機器人工廠、行星改造器等
|
||||||
|
|
||||||
|
### 科技系統
|
||||||
|
- **能量技術** - 提高能量效率
|
||||||
|
- **激光技術** - 增強武器系統
|
||||||
|
- **離子技術** - 高級推進和武器
|
||||||
|
- **超空間技術** - 實現更快的旅行
|
||||||
|
- **等離子技術** - 終極武器系統
|
||||||
|
- 還有更多...
|
||||||
|
|
||||||
|
### 艦船類別
|
||||||
|
- **民用艦船** - 小型/大型貨船、殖民船、回收船
|
||||||
|
- **戰鬥艦船** - 輕型/重型戰鬥機、巡洋艦、戰列艦、轟炸機
|
||||||
|
- **特殊艦船** - 死星、戰列巡洋艦、毀滅者
|
||||||
|
|
||||||
|
### 防禦系統
|
||||||
|
- 火箭發射器、輕型/重型激光炮、高斯炮
|
||||||
|
- 離子炮、等離子炮塔
|
||||||
|
- 小型/大型防護罩
|
||||||
|
|
||||||
|
## 數據安全
|
||||||
|
|
||||||
|
所有遊戲數據在儲存到瀏覽器的本地存儲之前都會使用 AES 加密自動加密。您的遊戲進度是安全且私密的。
|
||||||
|
|
||||||
|
## 自定義
|
||||||
|
|
||||||
|
應用支援通過 `src/style.css` 中定義的 Tailwind CSS 變數進行完整的主題自定義。您可以輕鬆地在淺色和深色模式之間切換。
|
||||||
|
|
||||||
|
## 貢獻
|
||||||
|
|
||||||
|
歡迎貢獻!請隨時提交 issue 或 pull request。
|
||||||
|
|
||||||
|
## 許可證
|
||||||
|
|
||||||
|
本作品採用 [創用CC 姓名標示-非商業性 4.0 國際 授權條款](https://creativecommons.org/licenses/by-nc/4.0/) 授權。
|
||||||
|
|
||||||
|
### 您可以自由地:
|
||||||
|
- **分享** — 以任何媒介或格式重製及散布本素材
|
||||||
|
- **修改** — 重混、轉換本素材、及依本素材建立新素材
|
||||||
|
|
||||||
|
### 惟需遵照下列條件:
|
||||||
|
- **姓名標示** — 您必須給予適當表彰、提供指向本授權條款的連結,以及指出是否已對本素材進行變更
|
||||||
|
- **非商業性** — 您不得將本素材用於商業目的
|
||||||
|
|
||||||
|
## 致謝
|
||||||
|
|
||||||
|
本專案受原版 [OGame](https://ogame.org/) 瀏覽器遊戲啟發。所有遊戲機制和設計元素都是為了教育和娛樂目的而重新實現的。
|
||||||
|
|
||||||
|
## 免責聲明
|
||||||
|
|
||||||
|
本專案與 Gameforge AG 或官方 OGame 遊戲沒有任何關聯、認可或聯繫。這是一個獨立的粉絲專案,創建目的僅用於教育和個人娛樂。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
用 ❤️ 製作,作者:<a href="https://github.com/setube">setube</a>
|
||||||
|
<br>
|
||||||
|
© 2025 - 保留所有權利(除 CC BY-NC 4.0 許可證授予的權利外)
|
||||||
|
</div>
|
||||||
106
README.md
@@ -5,13 +5,28 @@
|
|||||||
|
|
||||||
一个基于 Vue 3 和 TypeScript 构建的现代化 OGame 太空策略游戏。
|
一个基于 Vue 3 和 TypeScript 构建的现代化 OGame 太空策略游戏。
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest)
|
||||||
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
||||||
[](https://vuejs.org/)
|
[](https://vuejs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://vitejs.dev/)
|
[](https://vitejs.dev/)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
|
|
||||||
[English](README-EN.md) | 简体中文
|
**下载**
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-x86_64.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-arm64-v8a.APK)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-armeabi-v7a.APK)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-win.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/ogame-server-linux-arm64)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-Setup.exe)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-linux.AppImage)
|
||||||
|
[](https://github.com/setube/ogame-vue-ts/releases/latest/download/OGame-mac.dmg)
|
||||||
|
|
||||||
|
简体中文 | [English](README-EN.md) | [繁體中文](README-zh-TW.md) | [Deutsch](README-DE.md) | [Русский](README-RU.md) | [한국어](README-KO.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,28 +64,6 @@ OGame Vue TS 是一款受经典 OGame 游戏启发的单机版、基于浏览器
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 下载构建版本
|
|
||||||
|
|
||||||
#### 服务端
|
|
||||||
|
|
||||||
[Windows](/releases/latest/download/ogame-windows-amd64.exe)
|
|
||||||
|
|
||||||
[Linux amd64](/releases/latest/download/ogame-linux-amd64)
|
|
||||||
|
|
||||||
[Linux arm64](/releases/latest/download/ogame-linux-arm64)
|
|
||||||
|
|
||||||
[MacOS Intel](/releases/latest/download/ogame-macos-amd64)
|
|
||||||
|
|
||||||
[MacOS](/releases/latest/download/ogame-macos-arm64)
|
|
||||||
|
|
||||||
#### 桌面版
|
|
||||||
|
|
||||||
[Windows](/releases/latest/download/OGame.Setup.exe)
|
|
||||||
|
|
||||||
[Ubuntu](/releases/latest/download/OGame.AppImage)
|
|
||||||
|
|
||||||
[MacOS](/releases/latest/download/OGame-mac.dmg)
|
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (推荐 18 或更高版本)
|
- [Node.js](https://nodejs.org/) (推荐 18 或更高版本)
|
||||||
@@ -109,65 +102,14 @@ pnpm build
|
|||||||
pnpm preview
|
pnpm preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
ogame-vue-ts/
|
|
||||||
├── public/ # 静态资源
|
|
||||||
│ └── logo.svg # 应用图标
|
|
||||||
├── src/
|
|
||||||
│ ├── assets/ # 动态资源
|
|
||||||
│ ├── components/ # Vue 组件
|
|
||||||
│ │ └── ui/ # shadcn-vue UI 组件
|
|
||||||
│ ├── composables/ # Vue 组合式函数
|
|
||||||
│ ├── config/ # 游戏配置
|
|
||||||
│ ├── lib/ # 工具库
|
|
||||||
│ ├── locales/ # 国际化翻译文件
|
|
||||||
│ ├── logic/ # 游戏逻辑模块
|
|
||||||
│ │ ├── buildingLogic.ts # 建筑逻辑
|
|
||||||
│ │ ├── buildingValidation.ts # 建筑验证
|
|
||||||
│ │ ├── fleetLogic.ts # 舰队逻辑
|
|
||||||
│ │ ├── moonLogic.ts # 月球逻辑
|
|
||||||
│ │ ├── moonValidation.ts # 月球验证
|
|
||||||
│ │ ├── researchLogic.ts # 研究逻辑
|
|
||||||
│ │ ├── researchValidation.ts # 研究验证
|
|
||||||
│ │ ├── shipLogic.ts # 舰船逻辑
|
|
||||||
│ │ └── shipValidation.ts # 舰船验证
|
|
||||||
│ ├── router/ # Vue Router 路由配置
|
|
||||||
│ ├── stores/ # Pinia 状态存储
|
|
||||||
│ ├── types/ # TypeScript 类型定义
|
|
||||||
│ ├── utils/ # 工具函数
|
|
||||||
│ ├── views/ # 页面组件
|
|
||||||
│ │ ├── OverviewView.vue # 概览页面
|
|
||||||
│ │ ├── BuildingsView.vue # 建筑页面
|
|
||||||
│ │ ├── ResearchView.vue # 研究页面
|
|
||||||
│ │ ├── ShipyardView.vue # 船坞页面
|
|
||||||
│ │ ├── DefenseView.vue # 防御页面
|
|
||||||
│ │ ├── FleetView.vue # 舰队页面
|
|
||||||
│ │ ├── GalaxyView.vue # 银河页面
|
|
||||||
│ │ ├── OfficersView.vue # 军官页面
|
|
||||||
│ │ ├── BattleSimulatorView.vue # 战斗模拟器
|
|
||||||
│ │ ├── MessagesView.vue # 消息页面
|
|
||||||
│ │ └── SettingsView.vue # 设置页面
|
|
||||||
│ ├── App.vue # 根组件
|
|
||||||
│ ├── main.ts # 应用入口
|
|
||||||
│ └── style.css # 全局样式
|
|
||||||
├── .github/
|
|
||||||
│ └── ISSUE_TEMPLATE/ # GitHub issue 模板
|
|
||||||
├── LICENSE # CC BY-NC 4.0 许可证
|
|
||||||
├── package.json # 项目依赖
|
|
||||||
├── tsconfig.json # TypeScript 配置
|
|
||||||
└── vite.config.ts # Vite 配置
|
|
||||||
```
|
|
||||||
|
|
||||||
## 支持的语言
|
## 支持的语言
|
||||||
|
|
||||||
- 🇺🇸 English (英语)
|
- 简体中文
|
||||||
- 🇨🇳 简体中文
|
- 繁體中文
|
||||||
- 🇹🇼 繁體中文
|
- English (英语)
|
||||||
- 🇩🇪 Deutsch (德语)
|
- Deutsch (德语)
|
||||||
- 🇷🇺 Русский (俄语)
|
- Русский (俄语)
|
||||||
- 🇰🇷 한국어 (韩语)
|
- 한국어 (韩语)
|
||||||
|
|
||||||
## 游戏特性
|
## 游戏特性
|
||||||
|
|
||||||
@@ -236,7 +178,7 @@ ogame-vue-ts/
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
用 ❤️ 制作,作者:谦君
|
用 ❤️ 制作,作者:<a href="https://github.com/setube">setube</a>
|
||||||
<br>
|
<br>
|
||||||
© 2025 - 保留所有权利(除 CC BY-NC 4.0 许可证授予的权利外)
|
© 2025 - 保留所有权利(除 CC BY-NC 4.0 许可证授予的权利外)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
101
android/.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
||||||
2
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
92
android/app/build.gradle
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "games.wenzi.ogame"
|
||||||
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "games.wenzi.ogame"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 14
|
||||||
|
versionName "1.4.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
aaptOptions {
|
||||||
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 ABI 拆分 APK (arm64-v8a, armeabi-v7a, x86_64)
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include "arm64-v8a", "armeabi-v7a", "x86_64"
|
||||||
|
universalApk false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
storeFile file('release.keystore')
|
||||||
|
storePassword 'ogame123'
|
||||||
|
keyAlias 'ogame'
|
||||||
|
keyPassword 'ogame123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个 ABI 设置不同的 versionCode
|
||||||
|
applicationVariants.configureEach { variant ->
|
||||||
|
variant.outputs.configureEach { output ->
|
||||||
|
def abiVersionCode = [
|
||||||
|
"armeabi-v7a": 1,
|
||||||
|
"arm64-v8a": 2,
|
||||||
|
"x86_64": 3
|
||||||
|
]
|
||||||
|
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||||
|
if (abi != null) {
|
||||||
|
output.versionCodeOverride = abiVersionCode[abi] * 1000 + defaultConfig.versionCode
|
||||||
|
output.outputFileName = "OGame Vue Ts-${abi}.APK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
||||||
19
android/app/capacitor.build.gradle
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
||||||
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
BIN
android/app/release.keystore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package games.wenzi.ogame;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Window;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// 设置状态栏颜色,防止 Capacitor 强制透明
|
||||||
|
Window window = getWindow();
|
||||||
|
window.setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar_color));
|
||||||
|
window.setNavigationBarColor(ContextCompat.getColor(this, R.color.status_bar_color));
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
12
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="status_bar_color">#1a1a2e</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
7
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">OGame Vue Ts</string>
|
||||||
|
<string name="title_activity_main">OGame Vue Ts</string>
|
||||||
|
<string name="package_name">games.wenzi.ogame</string>
|
||||||
|
<string name="custom_url_scheme">games.wenzi.ogame</string>
|
||||||
|
</resources>
|
||||||
24
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">#1a1a2e</item>
|
||||||
|
<item name="colorPrimaryDark">#0f0f1a</item>
|
||||||
|
<item name="colorAccent">#6366f1</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:statusBarColor">#1a1a2e</item>
|
||||||
|
<item name="android:navigationBarColor">#1a1a2e</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
8
android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
android/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
3
android/capacitor.settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/android/capacitor')
|
||||||
22
android/gradle.properties
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
251
android/gradlew
vendored
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
94
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
5
android/settings.gradle
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
||||||
16
android/variables.gradle
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 36
|
||||||
|
targetSdkVersion = 36
|
||||||
|
androidxActivityVersion = '1.11.0'
|
||||||
|
androidxAppCompatVersion = '1.7.1'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||||
|
androidxCoreVersion = '1.17.0'
|
||||||
|
androidxFragmentVersion = '1.8.9'
|
||||||
|
coreSplashScreenVersion = '1.2.0'
|
||||||
|
androidxWebkitVersion = '1.14.0'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.3.0'
|
||||||
|
androidxEspressoCoreVersion = '3.7.0'
|
||||||
|
cordovaAndroidVersion = '14.0.1'
|
||||||
|
}
|
||||||
18
capacitor.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli'
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'games.wenzi.ogame',
|
||||||
|
appName: 'OGame Vue Ts',
|
||||||
|
webDir: 'docs',
|
||||||
|
server: {
|
||||||
|
androidScheme: 'https'
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
buildOptions: {
|
||||||
|
keystorePath: undefined,
|
||||||
|
keystoreAlias: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
13
package.json
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"buildDate": "2025/12/19 12:29:46",
|
"buildDate": "2025/12/23 19:38:12",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,9 +17,16 @@
|
|||||||
"build": "vue-tsc -b && vite build && node update-build-date.js",
|
"build": "vue-tsc -b && vite build && node update-build-date.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build:server": "pnpm run build && go build",
|
"build:server": "pnpm run build && go build",
|
||||||
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder"
|
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder",
|
||||||
|
"build:android": "pnpm run build && npx cap sync android",
|
||||||
|
"build:apk": "pnpm run build:android && cd android && ./gradlew assembleRelease"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor/android": "^8.0.0",
|
||||||
|
"@capacitor/app": "^8.0.0",
|
||||||
|
"@capacitor/cli": "^8.0.0",
|
||||||
|
"@capacitor/core": "^8.0.0",
|
||||||
|
"@capacitor/filesystem": "^8.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tanstack/vue-table": "^8.21.3",
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
@@ -75,7 +82,7 @@
|
|||||||
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
|
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "games.wenzi.ogame",
|
"appId": "games.wenzi.ogame",
|
||||||
"productName": "OGame",
|
"productName": "OGame Vue Ts",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "pkg"
|
"output": "pkg"
|
||||||
},
|
},
|
||||||
|
|||||||
392
pnpm-lock.yaml
generated
@@ -11,6 +11,21 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@capacitor/android':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0(@capacitor/core@8.0.0)
|
||||||
|
'@capacitor/app':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0(@capacitor/core@8.0.0)
|
||||||
|
'@capacitor/cli':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
|
'@capacitor/core':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
|
'@capacitor/filesystem':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0(@capacitor/core@8.0.0)
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
|
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
|
||||||
@@ -630,6 +645,32 @@ packages:
|
|||||||
'@canvas/image-data@1.1.0':
|
'@canvas/image-data@1.1.0':
|
||||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||||
|
|
||||||
|
'@capacitor/android@8.0.0':
|
||||||
|
resolution: {integrity: sha512-FrBSvVAC5JuLaYHNyDnwQny0/SYnP+xDQbc/KA4wInmRkMXLDv22fkx9aBJIDrxjuUVd+jsRih4SAt8FgMEzCw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': ^8.0.0
|
||||||
|
|
||||||
|
'@capacitor/app@8.0.0':
|
||||||
|
resolution: {integrity: sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': '>=8.0.0'
|
||||||
|
|
||||||
|
'@capacitor/cli@8.0.0':
|
||||||
|
resolution: {integrity: sha512-v9hEBi69xGxuuZhg55N031bMEenKaPSv71Il8C22VOOH6surDyv/MPeImN0oVfFc7eiklaW3rDFYVz6cmXfJWQ==}
|
||||||
|
engines: {node: '>=22.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@capacitor/core@8.0.0':
|
||||||
|
resolution: {integrity: sha512-250HTVd/W/KdMygoqaedisvNbHbpbQTN2Hy/8ZYGm1nAqE0Fx7sGss4l0nDg33STxEdDhtVRoL2fIaaiukKseA==}
|
||||||
|
|
||||||
|
'@capacitor/filesystem@8.0.0':
|
||||||
|
resolution: {integrity: sha512-RRGNLW9xEqvVVHGyGlfS4Oy0R3Na+bEefwZElKbex22S9eZr5cg8wc750BPPVwbcv5lf5fJymkY8x8y6UwKPyg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@capacitor/core': '>=8.0.0'
|
||||||
|
|
||||||
|
'@capacitor/synapse@1.0.4':
|
||||||
|
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
|
||||||
|
|
||||||
'@develar/schema-utils@2.6.5':
|
'@develar/schema-utils@2.6.5':
|
||||||
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
|
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
|
||||||
engines: {node: '>= 8.9.0'}
|
engines: {node: '>= 8.9.0'}
|
||||||
@@ -984,6 +1025,38 @@ packages:
|
|||||||
'@internationalized/number@3.6.5':
|
'@internationalized/number@3.6.5':
|
||||||
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
|
||||||
|
|
||||||
|
'@ionic/cli-framework-output@2.2.8':
|
||||||
|
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-array@2.1.6':
|
||||||
|
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-fs@3.1.7':
|
||||||
|
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-object@2.1.6':
|
||||||
|
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-process@2.1.12':
|
||||||
|
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-stream@3.1.7':
|
||||||
|
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-subprocess@3.0.1':
|
||||||
|
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
'@ionic/utils-terminal@2.3.5':
|
||||||
|
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1':
|
'@isaacs/balanced-match@4.0.1':
|
||||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -1339,6 +1412,9 @@ packages:
|
|||||||
'@types/file-saver@2.0.7':
|
'@types/file-saver@2.0.7':
|
||||||
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
||||||
|
|
||||||
|
'@types/fs-extra@8.1.5':
|
||||||
|
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||||
|
|
||||||
@@ -1366,6 +1442,9 @@ packages:
|
|||||||
'@types/responselike@1.0.3':
|
'@types/responselike@1.0.3':
|
||||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||||
|
|
||||||
|
'@types/slice-ansi@4.0.0':
|
||||||
|
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
@@ -1619,6 +1698,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
|
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
big-integer@1.6.52:
|
||||||
|
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
birpc@2.9.0:
|
birpc@2.9.0:
|
||||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||||
|
|
||||||
@@ -1629,6 +1712,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
|
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
|
||||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||||
|
|
||||||
|
bplist-parser@0.3.2:
|
||||||
|
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
|
||||||
|
engines: {node: '>= 5.10.0'}
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||||
|
|
||||||
@@ -1757,6 +1844,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@@ -1872,6 +1963,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1963,6 +2058,10 @@ packages:
|
|||||||
engines: {node: '>= 12.20.55'}
|
engines: {node: '>= 12.20.55'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
elementtree@0.1.7:
|
||||||
|
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
@@ -2206,6 +2305,10 @@ packages:
|
|||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
glob@13.0.0:
|
||||||
|
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Glob versions prior to v9 are no longer supported
|
||||||
@@ -2337,6 +2440,10 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ini@4.1.3:
|
||||||
|
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
internal-slot@1.1.0:
|
internal-slot@1.1.0:
|
||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2384,6 +2491,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-docker@2.2.1:
|
||||||
|
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
|
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2474,6 +2586,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isarray@2.0.5:
|
isarray@2.0.5:
|
||||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||||
|
|
||||||
@@ -2549,6 +2665,14 @@ packages:
|
|||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
kleur@3.0.3:
|
||||||
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
kleur@4.1.5:
|
||||||
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
lazy-val@1.0.5:
|
lazy-val@1.0.5:
|
||||||
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
||||||
|
|
||||||
@@ -2815,6 +2939,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
native-run@2.0.1:
|
||||||
|
resolution: {integrity: sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
negotiator@0.6.4:
|
negotiator@0.6.4:
|
||||||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2867,6 +2996,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ora@5.4.1:
|
ora@5.4.1:
|
||||||
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3005,6 +3138,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
|
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
prompts@2.4.2:
|
||||||
|
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
@@ -3109,6 +3246,11 @@ packages:
|
|||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rimraf@6.1.2:
|
||||||
|
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
roarr@2.15.4:
|
roarr@2.15.4:
|
||||||
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -3184,6 +3326,9 @@ packages:
|
|||||||
sanitize-filename@1.6.3:
|
sanitize-filename@1.6.3:
|
||||||
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
||||||
|
|
||||||
|
sax@1.1.4:
|
||||||
|
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
|
||||||
|
|
||||||
sax@1.4.3:
|
sax@1.4.3:
|
||||||
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
|
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
|
||||||
|
|
||||||
@@ -3278,10 +3423,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
sisteransi@1.0.5:
|
||||||
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
slice-ansi@3.0.0:
|
slice-ansi@3.0.0:
|
||||||
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
|
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
smart-buffer@4.2.0:
|
smart-buffer@4.2.0:
|
||||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||||
@@ -3321,6 +3473,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
sprintf-js@1.1.3:
|
sprintf-js@1.1.3:
|
||||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||||
|
|
||||||
@@ -3433,6 +3589,9 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
tiny-async-pool@1.3.0:
|
tiny-async-pool@1.3.0:
|
||||||
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
|
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
|
||||||
|
|
||||||
@@ -3457,6 +3616,10 @@ packages:
|
|||||||
tr46@1.0.1:
|
tr46@1.0.1:
|
||||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||||
|
|
||||||
@@ -3547,6 +3710,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
|
untildify@4.0.0:
|
||||||
|
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
upath@1.2.0:
|
upath@1.2.0:
|
||||||
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
|
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3730,6 +3897,14 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1:
|
||||||
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
xmlbuilder@15.1.1:
|
xmlbuilder@15.1.1:
|
||||||
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
|
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -4427,6 +4602,47 @@ snapshots:
|
|||||||
'@canvas/image-data@1.1.0':
|
'@canvas/image-data@1.1.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@capacitor/android@8.0.0(@capacitor/core@8.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.0.0
|
||||||
|
|
||||||
|
'@capacitor/app@8.0.0(@capacitor/core@8.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.0.0
|
||||||
|
|
||||||
|
'@capacitor/cli@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/cli-framework-output': 2.2.8
|
||||||
|
'@ionic/utils-subprocess': 3.0.1
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
commander: 12.1.0
|
||||||
|
debug: 4.4.3
|
||||||
|
env-paths: 2.2.1
|
||||||
|
fs-extra: 11.3.2
|
||||||
|
kleur: 4.1.5
|
||||||
|
native-run: 2.0.1
|
||||||
|
open: 8.4.2
|
||||||
|
plist: 3.1.0
|
||||||
|
prompts: 2.4.2
|
||||||
|
rimraf: 6.1.2
|
||||||
|
semver: 7.7.3
|
||||||
|
tar: 6.2.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
xml2js: 0.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@capacitor/core@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@capacitor/filesystem@8.0.0(@capacitor/core@8.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@capacitor/core': 8.0.0
|
||||||
|
'@capacitor/synapse': 1.0.4
|
||||||
|
|
||||||
|
'@capacitor/synapse@1.0.4': {}
|
||||||
|
|
||||||
'@develar/schema-utils@2.6.5':
|
'@develar/schema-utils@2.6.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
@@ -4741,6 +4957,82 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.17
|
'@swc/helpers': 0.5.17
|
||||||
|
|
||||||
|
'@ionic/cli-framework-output@2.2.8':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-array@2.1.6':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-fs@3.1.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/fs-extra': 8.1.5
|
||||||
|
debug: 4.4.3
|
||||||
|
fs-extra: 9.1.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-object@2.1.6':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-process@2.1.12':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-object': 2.1.6
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
debug: 4.4.3
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-stream@3.1.7':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-subprocess@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-array': 2.1.6
|
||||||
|
'@ionic/utils-fs': 3.1.7
|
||||||
|
'@ionic/utils-process': 2.1.12
|
||||||
|
'@ionic/utils-stream': 3.1.7
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
debug: 4.4.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@ionic/utils-terminal@2.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/slice-ansi': 4.0.0
|
||||||
|
debug: 4.4.3
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
slice-ansi: 4.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
untildify: 4.0.0
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1': {}
|
'@isaacs/balanced-match@4.0.1': {}
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.0':
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
@@ -5041,6 +5333,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/file-saver@2.0.7': {}
|
'@types/file-saver@2.0.7': {}
|
||||||
|
|
||||||
|
'@types/fs-extra@8.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.10.2
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.2
|
'@types/node': 24.10.2
|
||||||
@@ -5073,6 +5369,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.2
|
'@types/node': 24.10.2
|
||||||
|
|
||||||
|
'@types/slice-ansi@4.0.0': {}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
'@types/verror@1.10.11':
|
'@types/verror@1.10.11':
|
||||||
@@ -5354,8 +5652,7 @@ snapshots:
|
|||||||
assert-plus@1.0.0:
|
assert-plus@1.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
astral-regex@2.0.0:
|
astral-regex@2.0.0: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
async-exit-hook@2.0.1: {}
|
async-exit-hook@2.0.1: {}
|
||||||
|
|
||||||
@@ -5401,6 +5698,8 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.9.7: {}
|
baseline-browser-mapping@2.9.7: {}
|
||||||
|
|
||||||
|
big-integer@1.6.52: {}
|
||||||
|
|
||||||
birpc@2.9.0: {}
|
birpc@2.9.0: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
@@ -5412,6 +5711,10 @@ snapshots:
|
|||||||
boolean@3.2.0:
|
boolean@3.2.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
bplist-parser@0.3.2:
|
||||||
|
dependencies:
|
||||||
|
big-integer: 1.6.52
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
@@ -5591,6 +5894,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@5.1.0: {}
|
commander@5.1.0: {}
|
||||||
@@ -5702,6 +6007,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
define-properties@1.2.1:
|
define-properties@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -5845,6 +6152,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
elementtree@0.1.7:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.1.4
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
@@ -6174,6 +6485,12 @@ snapshots:
|
|||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 2.0.1
|
path-scurry: 2.0.1
|
||||||
|
|
||||||
|
glob@13.0.0:
|
||||||
|
dependencies:
|
||||||
|
minimatch: 10.1.1
|
||||||
|
minipass: 7.1.2
|
||||||
|
path-scurry: 2.0.1
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
@@ -6332,6 +6649,8 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ini@4.1.3: {}
|
||||||
|
|
||||||
internal-slot@1.1.0:
|
internal-slot@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -6387,6 +6706,8 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -6465,6 +6786,10 @@ snapshots:
|
|||||||
|
|
||||||
is-what@5.5.0: {}
|
is-what@5.5.0: {}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 2.2.1
|
||||||
|
|
||||||
isarray@2.0.5: {}
|
isarray@2.0.5: {}
|
||||||
|
|
||||||
isbinaryfile@4.0.10: {}
|
isbinaryfile@4.0.10: {}
|
||||||
@@ -6528,6 +6853,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
||||||
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
|
kleur@4.1.5: {}
|
||||||
|
|
||||||
lazy-val@1.0.5: {}
|
lazy-val@1.0.5: {}
|
||||||
|
|
||||||
leven@3.1.0: {}
|
leven@3.1.0: {}
|
||||||
@@ -6758,6 +7087,22 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
native-run@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@ionic/utils-fs': 3.1.7
|
||||||
|
'@ionic/utils-terminal': 2.3.5
|
||||||
|
bplist-parser: 0.3.2
|
||||||
|
debug: 4.4.3
|
||||||
|
elementtree: 0.1.7
|
||||||
|
ini: 4.1.3
|
||||||
|
plist: 3.1.0
|
||||||
|
split2: 4.2.0
|
||||||
|
through2: 4.0.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
yauzl: 2.10.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
negotiator@0.6.4: {}
|
negotiator@0.6.4: {}
|
||||||
|
|
||||||
node-abi@3.85.0:
|
node-abi@3.85.0:
|
||||||
@@ -6806,6 +7151,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
dependencies:
|
||||||
|
define-lazy-prop: 2.0.0
|
||||||
|
is-docker: 2.2.1
|
||||||
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
ora@5.4.1:
|
ora@5.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
bl: 4.1.0
|
bl: 4.1.0
|
||||||
@@ -6915,6 +7266,11 @@ snapshots:
|
|||||||
err-code: 2.0.3
|
err-code: 2.0.3
|
||||||
retry: 0.12.0
|
retry: 0.12.0
|
||||||
|
|
||||||
|
prompts@2.4.2:
|
||||||
|
dependencies:
|
||||||
|
kleur: 3.0.3
|
||||||
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
@@ -7040,6 +7396,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
rimraf@6.1.2:
|
||||||
|
dependencies:
|
||||||
|
glob: 13.0.0
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
|
||||||
roarr@2.15.4:
|
roarr@2.15.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolean: 3.2.0
|
boolean: 3.2.0
|
||||||
@@ -7117,6 +7478,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
truncate-utf8-bytes: 1.0.2
|
truncate-utf8-bytes: 1.0.2
|
||||||
|
|
||||||
|
sax@1.1.4: {}
|
||||||
|
|
||||||
sax@1.4.3: {}
|
sax@1.4.3: {}
|
||||||
|
|
||||||
semver-compare@1.0.0:
|
semver-compare@1.0.0:
|
||||||
@@ -7267,6 +7630,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
|
|
||||||
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
slice-ansi@3.0.0:
|
slice-ansi@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
@@ -7274,6 +7639,12 @@ snapshots:
|
|||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
slice-ansi@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
astral-regex: 2.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0: {}
|
||||||
|
|
||||||
smob@1.5.0: {}
|
smob@1.5.0: {}
|
||||||
@@ -7308,6 +7679,8 @@ snapshots:
|
|||||||
|
|
||||||
speakingurl@14.0.1: {}
|
speakingurl@14.0.1: {}
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
sprintf-js@1.1.3:
|
sprintf-js@1.1.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7452,6 +7825,10 @@ snapshots:
|
|||||||
commander: 2.20.3
|
commander: 2.20.3
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
|
|
||||||
|
through2@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
tiny-async-pool@1.3.0:
|
tiny-async-pool@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 5.7.2
|
semver: 5.7.2
|
||||||
@@ -7476,6 +7853,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
utf8-byte-length: 1.0.5
|
utf8-byte-length: 1.0.5
|
||||||
@@ -7577,6 +7956,8 @@ snapshots:
|
|||||||
|
|
||||||
universalify@2.0.1: {}
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
|
untildify@4.0.0: {}
|
||||||
|
|
||||||
upath@1.2.0: {}
|
upath@1.2.0: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.2(browserslist@4.28.1):
|
update-browserslist-db@1.2.2(browserslist@4.28.1):
|
||||||
@@ -7832,6 +8213,13 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.4.3
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1: {}
|
xmlbuilder@15.1.1: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|||||||
BIN
resources/icon.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
778
src/App.vue
@@ -28,23 +28,23 @@
|
|||||||
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
|
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2.5 flex-1 min-w-0">
|
<div class="flex items-start gap-2.5 flex-1 min-w-0">
|
||||||
<Globe class="h-5 w-5 flex-shrink-0 mt-0.5 text-primary" />
|
<Globe class="h-5 w-5 shrink-0 mt-0.5 text-primary" />
|
||||||
<div class="flex-1 min-w-0 text-left">
|
<div class="flex-1 min-w-0 text-left">
|
||||||
<div class="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">
|
<div class="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">
|
||||||
{{ t('planet.currentPlanet') }}
|
{{ t('planet.currentPlanet') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 mb-0.5">
|
<div class="flex items-center gap-1.5 mb-0.5">
|
||||||
<span class="truncate font-semibold text-sm">{{ planet.name }}</span>
|
<span class="truncate font-semibold text-sm">
|
||||||
|
{{ planet.name }}
|
||||||
|
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||||
|
</span>
|
||||||
<Badge v-if="planet.isMoon" variant="secondary" class="text-[10px] px-1 py-0 h-4">
|
<Badge v-if="planet.isMoon" variant="secondary" class="text-[10px] px-1 py-0 h-4">
|
||||||
{{ t('planet.moon') }}
|
{{ t('planet.moon') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown class="h-4 w-4 flex-shrink-0 text-muted-foreground ml-2" />
|
<ChevronsUpDown class="h-4 w-4 shrink-0 text-muted-foreground ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-72 p-0" side="bottom" align="start">
|
<PopoverContent class="w-72 p-0" side="bottom" align="start">
|
||||||
@@ -61,14 +61,17 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2 w-full min-w-0">
|
<div class="flex items-start gap-2 w-full min-w-0">
|
||||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" :class="p.id === planet.id ? 'text-primary' : ''" />
|
<Globe class="h-4 w-4 shrink-0 mt-0.5" :class="p.id === planet.id ? 'text-primary' : ''" />
|
||||||
<div class="flex-1 min-w-0 text-left">
|
<div class="flex-1 min-w-0 text-left">
|
||||||
<div class="flex items-center gap-1.5 mb-0.5">
|
<div class="flex items-center gap-1.5 mb-0.5">
|
||||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
<span class="truncate font-medium text-sm">
|
||||||
|
{{ p.name }}
|
||||||
|
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||||
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-2 w-2 p-0 flex-shrink-0"
|
class="h-2 w-2 p-0 shrink-0"
|
||||||
@click.stop="openRenameDialog(p.id, p.name)"
|
@click.stop="openRenameDialog(p.id, p.name)"
|
||||||
:title="t('planet.renamePlanet')"
|
:title="t('planet.renamePlanet')"
|
||||||
>
|
>
|
||||||
@@ -78,9 +81,6 @@
|
|||||||
{{ t('planet.moon') }}
|
{{ t('planet.moon') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,7 +117,8 @@
|
|||||||
:data-nav-path="item.path"
|
:data-nav-path="item.path"
|
||||||
:is-active="$route.path === item.path"
|
:is-active="$route.path === item.path"
|
||||||
:tooltip="item.name.value"
|
:tooltip="item.name.value"
|
||||||
@click="handleNavClick(item.path, $event)"
|
:disabled="!isFeatureUnlocked(item.path)"
|
||||||
|
@click="router.push(item.path)"
|
||||||
>
|
>
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
<span>{{ item.name.value }}</span>
|
<span>{{ item.name.value }}</span>
|
||||||
@@ -200,16 +201,26 @@
|
|||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<div class="flex flex-col h-full pt-[60px]">
|
<div class="flex flex-col h-full" :class="Capacitor.isNativePlatform() ? 'pt-[80px]' : 'pt-[60px]'">
|
||||||
<!-- 顶部资源栏 - 固定定位 -->
|
<!-- 顶部资源栏 - 固定定位 -->
|
||||||
<header
|
<header
|
||||||
v-if="planet"
|
v-if="planet"
|
||||||
class="fixed top-0 right-0 left-0 z-40 bg-card border-b px-4 sm:px-6 py-3 shadow-md"
|
ref="header"
|
||||||
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
|
class="fixed top-0 right-0 left-0 z-40 bg-card border-b px-4 sm:px-6 shadow-md"
|
||||||
|
:class="[
|
||||||
|
sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]',
|
||||||
|
Capacitor.isNativePlatform() ? 'py-6' : 'py-3'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<!-- 第一行:菜单、资源预览、状态 -->
|
<!-- 第一行:菜单、资源预览、状态 -->
|
||||||
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
|
<div
|
||||||
|
class="grid items-center gap-3 sm:gap-6"
|
||||||
|
style="grid-template-columns: auto 1fr auto"
|
||||||
|
:class="{
|
||||||
|
'relative top-3': Capacitor.isNativePlatform()
|
||||||
|
}"
|
||||||
|
>
|
||||||
<!-- 左侧:汉堡菜单(移动端)/ 占位(PC端) -->
|
<!-- 左侧:汉堡菜单(移动端)/ 占位(PC端) -->
|
||||||
<div>
|
<div>
|
||||||
<SidebarTrigger class="lg:hidden" data-tutorial="mobile-menu" />
|
<SidebarTrigger class="lg:hidden" data-tutorial="mobile-menu" />
|
||||||
@@ -220,25 +231,25 @@
|
|||||||
<div class="min-w-0 overflow-hidden">
|
<div class="min-w-0 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="resource-bar flex items-center gap-3 sm:gap-6 justify-start sm:justify-center"
|
class="resource-bar flex items-center gap-3 sm:gap-6 justify-start sm:justify-center"
|
||||||
:class="resourceBarExpanded ? 'hidden' : 'overflow-x-auto'"
|
:class="[resourceBarExpanded ? 'hidden' : 'overflow-x-auto']"
|
||||||
>
|
>
|
||||||
<div
|
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 shrink-0">
|
||||||
v-for="resourceType in resourceTypes"
|
|
||||||
:key="resourceType.key"
|
|
||||||
class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0"
|
|
||||||
>
|
|
||||||
<ResourceIcon :type="resourceType.key" size="md" />
|
<ResourceIcon :type="resourceType.key" size="md" />
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<!-- 电力显示净产量和效率 -->
|
<!-- 电力显示:当前储量/最大容量,净产量/小时 -->
|
||||||
<template v-if="resourceType.key === 'energy'">
|
<template v-if="resourceType.key === 'energy'">
|
||||||
<p
|
<p
|
||||||
class="text-xs sm:text-sm font-medium truncate"
|
class="text-xs sm:text-sm font-medium truncate"
|
||||||
|
:class="getResourceColor(planet.resources.energy, capacity?.energy || Infinity)"
|
||||||
|
>
|
||||||
|
{{ formatNumber(planet.resources.energy) }} /
|
||||||
|
{{ formatNumber(capacity?.energy || 0) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-[10px] sm:text-xs truncate"
|
||||||
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||||
>
|
>
|
||||||
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
|
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(Math.round(netEnergy / 60)) }}/{{ t('resources.perMinute') }}
|
||||||
</p>
|
|
||||||
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
|
|
||||||
{{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
|
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<!-- 其他资源统一显示:当前值/容量 -->
|
<!-- 其他资源统一显示:当前值/容量 -->
|
||||||
@@ -259,8 +270,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:展开按钮(仅移动端) -->
|
<!-- 右侧:队列通知 + 展开按钮 -->
|
||||||
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0 justify-end">
|
<div class="flex items-center gap-2 sm:gap-3 shrink-0 justify-end">
|
||||||
<!-- 移动端展开按钮 -->
|
<!-- 移动端展开按钮 -->
|
||||||
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
|
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
|
||||||
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
||||||
@@ -282,8 +293,11 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="planet && resourceBarExpanded"
|
v-if="planet && resourceBarExpanded"
|
||||||
class="fixed top-[60px] right-0 left-0 z-30 bg-card border-b px-4 py-3 shadow-md lg:hidden"
|
class="fixed right-0 left-0 z-30 bg-card border-b px-4 py-3 shadow-md lg:hidden"
|
||||||
:class="sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]'"
|
:class="[
|
||||||
|
sidebarOpen ? 'lg:left-[var(--sidebar-width)]' : 'lg:left-[var(--sidebar-width-icon)]',
|
||||||
|
Capacitor.isNativePlatform() ? 'top-[80px]' : 'top-[60px]'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="bg-muted/50 rounded-lg p-2.5">
|
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="bg-muted/50 rounded-lg p-2.5">
|
||||||
@@ -292,16 +306,21 @@
|
|||||||
<span class="text-xs font-medium text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}</span>
|
<span class="text-xs font-medium text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-0.5 text-center">
|
<div class="space-y-0.5 text-center">
|
||||||
<!-- 电力显示净产量和效率 -->
|
<!-- 电力显示:当前储量,容量,净产量/分钟 -->
|
||||||
<template v-if="resourceType.key === 'energy'">
|
<template v-if="resourceType.key === 'energy'">
|
||||||
<p
|
<p class="text-sm font-semibold" :class="getResourceColor(planet.resources.energy, capacity?.energy || Infinity)">
|
||||||
class="text-sm font-semibold"
|
{{ formatNumber(planet.resources.energy) }}
|
||||||
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
|
||||||
>
|
|
||||||
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[10px] text-muted-foreground">
|
<p class="text-[10px] text-muted-foreground">
|
||||||
{{ t('resources.production') }}: {{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
|
{{ t('resources.capacity') }}: {{ formatNumber(capacity?.energy || 0) }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-[10px]"
|
||||||
|
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||||
|
>
|
||||||
|
{{ t('resources.production') }}: {{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(Math.round(netEnergy / 60)) }}/{{
|
||||||
|
t('resources.perMinute')
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<!-- 其他资源统一显示:当前值/容量 -->
|
<!-- 其他资源统一显示:当前值/容量 -->
|
||||||
@@ -330,6 +349,12 @@
|
|||||||
<!-- 即将到来的敌对舰队警告 -->
|
<!-- 即将到来的敌对舰队警告 -->
|
||||||
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
|
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
|
||||||
|
|
||||||
|
<!-- 低电量警告 -->
|
||||||
|
<LowEnergyWarning />
|
||||||
|
|
||||||
|
<!-- 矿脉储量警告 -->
|
||||||
|
<OreDepositWarning />
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<main class="flex-1">
|
<main class="flex-1">
|
||||||
<Transition name="page" mode="out-in">
|
<Transition name="page" mode="out-in">
|
||||||
@@ -365,7 +390,6 @@
|
|||||||
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||||
<!-- 返回顶部 -->
|
<!-- 返回顶部 -->
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
|
|
||||||
<!-- 队列通知 -->
|
<!-- 队列通知 -->
|
||||||
<QueueNotifications />
|
<QueueNotifications />
|
||||||
|
|
||||||
@@ -391,19 +415,14 @@
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 详情弹窗 -->
|
||||||
<DetailDialog />
|
<DetailDialog />
|
||||||
|
|
||||||
<!-- 更新弹窗 -->
|
<!-- 更新弹窗 -->
|
||||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||||
|
|
||||||
<!-- 弱引导提示系统 -->
|
<!-- 弱引导提示系统 -->
|
||||||
<HintToast />
|
<HintToast />
|
||||||
|
|
||||||
<!-- Toast 通知 -->
|
<!-- Toast 通知 -->
|
||||||
<Sonner position="top-center" />
|
<Sonner position="top-center" />
|
||||||
|
|
||||||
<!-- 重命名星球对话框 -->
|
<!-- 重命名星球对话框 -->
|
||||||
<Dialog v-model:open="renameDialogOpen">
|
<Dialog v-model:open="renameDialogOpen">
|
||||||
<DialogContent class="sm:max-w-md">
|
<DialogContent class="sm:max-w-md">
|
||||||
@@ -425,6 +444,36 @@
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
||||||
|
<!-- Android 退出确认对话框 -->
|
||||||
|
<AlertDialog v-model:open="exitDialogOpen">
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{{ t('common.exitConfirmTitle') }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{{ t('common.exitConfirmMessage') }}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{{ t('common.cancel') }}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction @click="exitApp">{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<!-- NPC 名称更新确认对话框 -->
|
||||||
|
<AlertDialog v-model:open="npcNameUpdateDialogOpen">
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{{ t('settings.npcNameUpdateTitle') }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{{ t('settings.npcNameUpdateMessage', { count: oldFormatNPCCount }) }}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel @click="handleSkipNPCNameUpdate">{{ t('settings.npcNameUpdateCancel') }}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction @click="handleUpdateNPCNames">{{ t('settings.npcNameUpdateConfirm') }}</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -442,10 +491,12 @@
|
|||||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
|
import IncomingFleetAlerts from '@/components/notifications/IncomingFleetAlerts.vue'
|
||||||
import DiplomaticNotifications from '@/components/DiplomaticNotifications.vue'
|
import LowEnergyWarning from '@/components/notifications/LowEnergyWarning.vue'
|
||||||
import EnemyAlertNotifications from '@/components/EnemyAlertNotifications.vue'
|
import OreDepositWarning from '@/components/notifications/OreDepositWarning.vue'
|
||||||
import QueueNotifications from '@/components/QueueNotifications.vue'
|
import DiplomaticNotifications from '@/components/notifications/DiplomaticNotifications.vue'
|
||||||
|
import EnemyAlertNotifications from '@/components/notifications/EnemyAlertNotifications.vue'
|
||||||
|
import QueueNotifications from '@/components/notifications/QueueNotifications.vue'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -460,7 +511,7 @@
|
|||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger
|
SidebarTrigger
|
||||||
} from '@/components/ui/sidebar'
|
} from '@/components/ui/sidebar'
|
||||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -471,10 +522,10 @@
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import DetailDialog from '@/components/DetailDialog.vue'
|
import DetailDialog from '@/components/dialogs/DetailDialog.vue'
|
||||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
import UpdateDialog from '@/components/dialogs/UpdateDialog.vue'
|
||||||
import HintToast from '@/components/HintToast.vue'
|
import HintToast from '@/components/notifications/HintToast.vue'
|
||||||
import BackToTop from '@/components/BackToTop.vue'
|
import BackToTop from '@/components/common/BackToTop.vue'
|
||||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||||
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game'
|
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game'
|
||||||
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
||||||
@@ -503,7 +554,10 @@
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Handshake,
|
Handshake,
|
||||||
Pencil
|
Pencil,
|
||||||
|
Trophy,
|
||||||
|
Crown,
|
||||||
|
Scroll
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import * as gameLogic from '@/logic/gameLogic'
|
import * as gameLogic from '@/logic/gameLogic'
|
||||||
import * as planetLogic from '@/logic/planetLogic'
|
import * as planetLogic from '@/logic/planetLogic'
|
||||||
@@ -517,12 +571,17 @@
|
|||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||||
import * as publicLogic from '@/logic/publicLogic'
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
|
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
||||||
|
import * as campaignLogic from '@/logic/campaignLogic'
|
||||||
|
import { generateNPCName, countOldFormatNPCs, updateNPCName } from '@/logic/npcNameGenerator'
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { migrateGameData } from '@/utils/migration'
|
import { migrateGameData } from '@/utils/migration'
|
||||||
import { checkLatestVersion } from '@/utils/versionCheck'
|
import { checkLatestVersion } from '@/utils/versionCheck'
|
||||||
import { StarsBackground } from '@/components/ui/bg-stars'
|
import { StarsBackground } from '@/components/ui/bg-stars'
|
||||||
import { ParticlesBg } from '@/components/ui/particles-bg'
|
import { ParticlesBg } from '@/components/ui/particles-bg'
|
||||||
|
import { App as CapacitorApp } from '@capacitor/app'
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
|
||||||
// 执行数据迁移(在 store 初始化之前)
|
// 执行数据迁移(在 store 初始化之前)
|
||||||
migrateGameData()
|
migrateGameData()
|
||||||
@@ -565,12 +624,18 @@
|
|||||||
const renameDialogOpen = ref(false)
|
const renameDialogOpen = ref(false)
|
||||||
const renamingPlanetId = ref<string | null>(null)
|
const renamingPlanetId = ref<string | null>(null)
|
||||||
const newPlanetName = ref('')
|
const newPlanetName = ref('')
|
||||||
|
// Android 退出确认对话框状态
|
||||||
|
const exitDialogOpen = ref(false)
|
||||||
|
// NPC 名称更新对话框状态
|
||||||
|
const npcNameUpdateDialogOpen = ref(false)
|
||||||
|
const oldFormatNPCCount = ref(0)
|
||||||
// 功能解锁要求配置
|
// 功能解锁要求配置
|
||||||
const featureRequirements: Record<string, { building: BuildingType; level: number }> = {
|
const featureRequirements: Record<string, { building: BuildingType; level: number }> = {
|
||||||
'/research': { building: BuildingType.ResearchLab, level: 1 },
|
'/research': { building: BuildingType.ResearchLab, level: 1 },
|
||||||
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
|
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
|
||||||
'/defense': { building: BuildingType.Shipyard, level: 1 },
|
'/defense': { building: BuildingType.Shipyard, level: 1 },
|
||||||
'/fleet': { building: BuildingType.Shipyard, level: 1 }
|
'/fleet': { building: BuildingType.Shipyard, level: 1 },
|
||||||
|
'/officers': { building: BuildingType.Shipyard, level: 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为首页
|
// 判断是否为首页
|
||||||
@@ -599,6 +664,9 @@
|
|||||||
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
||||||
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
||||||
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
||||||
|
{ name: computed(() => t('nav.achievements')), path: '/achievements', icon: Trophy },
|
||||||
|
{ name: computed(() => t('nav.campaign')), path: '/campaign', icon: Scroll },
|
||||||
|
{ name: computed(() => t('nav.ranking')), path: '/ranking', icon: Crown },
|
||||||
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
||||||
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
||||||
// GM菜单在启用GM模式时显示
|
// GM菜单在启用GM模式时显示
|
||||||
@@ -699,7 +767,7 @@
|
|||||||
|
|
||||||
if (!settings.types[typeKey]) return
|
if (!settings.types[typeKey]) return
|
||||||
|
|
||||||
// browser
|
// 浏览器通知
|
||||||
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
|
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
|
||||||
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
|
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
|
||||||
if (!shouldSuppress) {
|
if (!shouldSuppress) {
|
||||||
@@ -707,12 +775,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// toast
|
// 页面内 toast 通知
|
||||||
if (settings.inApp) {
|
if (settings.inApp) {
|
||||||
toast.success(title, { description: body })
|
toast.success(title, { description: body })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理解锁通知
|
||||||
|
const handleUnlockNotification = (unlockedItems: Array<{ type: 'building' | 'technology'; id: string; name: string }>) => {
|
||||||
|
const settings = gameStore.notificationSettings
|
||||||
|
if (!settings) return
|
||||||
|
|
||||||
|
// 检查主开关和解锁类型开关
|
||||||
|
if (!settings.browser && !settings.inApp) return
|
||||||
|
if (!settings.types.unlock) return
|
||||||
|
|
||||||
|
unlockedItems.forEach(item => {
|
||||||
|
const title = t('notifications.newUnlock')
|
||||||
|
const typeLabel = item.type === 'building' ? t('notifications.building') : t('notifications.technology')
|
||||||
|
const body = `${typeLabel}: ${item.name}`
|
||||||
|
|
||||||
|
// 浏览器通知
|
||||||
|
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
|
||||||
|
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
|
||||||
|
if (!shouldSuppress) {
|
||||||
|
new Notification(title, { body, icon: '/favicon.ico' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面内 toast 通知
|
||||||
|
if (settings.inApp) {
|
||||||
|
toast.info(title, { description: body })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleConfirmDialogConfirm = () => {
|
const handleConfirmDialogConfirm = () => {
|
||||||
if (confirmDialogAction.value) {
|
if (confirmDialogAction.value) {
|
||||||
confirmDialogAction.value()
|
confirmDialogAction.value()
|
||||||
@@ -724,6 +821,20 @@
|
|||||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||||
if (!shouldInit) {
|
if (!shouldInit) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
// 迁移矿脉储量数据(为没有矿脉数据的星球初始化)
|
||||||
|
gameStore.player.planets.forEach(planet => {
|
||||||
|
oreDepositLogic.migrateOreDeposits(planet)
|
||||||
|
})
|
||||||
|
// 迁移NPC星球的矿脉储量
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
npc.planets.forEach(planet => {
|
||||||
|
oreDepositLogic.migrateOreDeposits(planet)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 迁移宇宙地图中的星球(NPC星球的副本)
|
||||||
|
Object.values(universeStore.planets).forEach(planet => {
|
||||||
|
oreDepositLogic.migrateOreDeposits(planet)
|
||||||
|
})
|
||||||
|
|
||||||
// 计算离线收益(直接同步计算,应用游戏速度)
|
// 计算离线收益(直接同步计算,应用游戏速度)
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||||
@@ -769,7 +880,7 @@
|
|||||||
// 检查军官过期
|
// 检查军官过期
|
||||||
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
||||||
// 处理游戏更新(建造队列、研究队列等)
|
// 处理游戏更新(建造队列、研究队列等)
|
||||||
const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification)
|
const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification, handleUnlockNotification)
|
||||||
gameStore.player.researchQueue = result.updatedResearchQueue
|
gameStore.player.researchQueue = result.updatedResearchQueue
|
||||||
// 处理舰队任务
|
// 处理舰队任务
|
||||||
gameStore.player.fleetMissions.forEach(mission => {
|
gameStore.player.fleetMissions.forEach(mission => {
|
||||||
@@ -809,23 +920,43 @@
|
|||||||
// NPC行为系统更新(侦查和攻击决策)
|
// NPC行为系统更新(侦查和攻击决策)
|
||||||
updateNPCBehavior(1)
|
updateNPCBehavior(1)
|
||||||
|
|
||||||
|
// 检查成就解锁
|
||||||
|
checkAchievementUnlocks()
|
||||||
|
|
||||||
|
// 检查战役任务进度
|
||||||
|
if (gameStore.player.campaignProgress) {
|
||||||
|
campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
// 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
||||||
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
|
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
|
||||||
if (eliminatedNpcIds.length > 0) {
|
if (eliminatedNpcIds.length > 0) {
|
||||||
// 从universeStore中移除被消灭NPC的星球数据
|
// 从universeStore中移除被消灭NPC的星球数据,并收集需要清理的任务ID
|
||||||
|
const missionIdsToRemove: string[] = []
|
||||||
eliminatedNpcIds.forEach(npcId => {
|
eliminatedNpcIds.forEach(npcId => {
|
||||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||||
if (npc && npc.planets) {
|
if (npc) {
|
||||||
// 遍历NPC的所有星球,从universeStore中删除
|
// 遍历NPC的所有星球,从universeStore中删除
|
||||||
npc.planets.forEach(planet => {
|
if (npc.planets) {
|
||||||
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
|
npc.planets.forEach(planet => {
|
||||||
if (universeStore.planets[planetKey]) {
|
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
|
||||||
delete universeStore.planets[planetKey]
|
if (universeStore.planets[planetKey]) {
|
||||||
}
|
delete universeStore.planets[planetKey]
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 收集该NPC所有任务的ID(用于清理玩家的警报)
|
||||||
|
if (npc.fleetMissions) {
|
||||||
|
npc.fleetMissions.forEach(m => missionIdsToRemove.push(m.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 清理玩家的即将到来舰队警报(移除已消灭NPC的任务警报)
|
||||||
|
if (gameStore.player.incomingFleetAlerts && missionIdsToRemove.length > 0) {
|
||||||
|
gameStore.player.incomingFleetAlerts = gameStore.player.incomingFleetAlerts.filter(alert => !missionIdsToRemove.includes(alert.id))
|
||||||
|
}
|
||||||
|
|
||||||
// 从NPC列表中移除被消灭的NPC
|
// 从NPC列表中移除被消灭的NPC
|
||||||
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
|
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
|
||||||
}
|
}
|
||||||
@@ -839,13 +970,28 @@
|
|||||||
mission.targetPosition.position
|
mission.targetPosition.position
|
||||||
)
|
)
|
||||||
// 先从玩家星球中查找,再从宇宙地图中查找
|
// 先从玩家星球中查找,再从宇宙地图中查找
|
||||||
|
// 如果任务指定了targetIsMoon,需要精确匹配行星或月球
|
||||||
const targetPlanet =
|
const targetPlanet =
|
||||||
|
gameStore.player.planets.find(p => {
|
||||||
|
const positionMatch =
|
||||||
|
p.position.galaxy === mission.targetPosition.galaxy &&
|
||||||
|
p.position.system === mission.targetPosition.system &&
|
||||||
|
p.position.position === mission.targetPosition.position
|
||||||
|
// 如果任务明确指定目标类型,按类型匹配
|
||||||
|
if (mission.targetIsMoon !== undefined) {
|
||||||
|
return positionMatch && p.isMoon === mission.targetIsMoon
|
||||||
|
}
|
||||||
|
// 兼容旧任务:默认优先匹配行星(非月球)
|
||||||
|
return positionMatch && !p.isMoon
|
||||||
|
}) ||
|
||||||
|
// 如果没有匹配到指定类型,尝试匹配同位置的任何星球
|
||||||
gameStore.player.planets.find(
|
gameStore.player.planets.find(
|
||||||
p =>
|
p =>
|
||||||
p.position.galaxy === mission.targetPosition.galaxy &&
|
p.position.galaxy === mission.targetPosition.galaxy &&
|
||||||
p.position.system === mission.targetPosition.system &&
|
p.position.system === mission.targetPosition.system &&
|
||||||
p.position.position === mission.targetPosition.position
|
p.position.position === mission.targetPosition.position
|
||||||
) || universeStore.planets[targetKey]
|
) ||
|
||||||
|
universeStore.planets[targetKey]
|
||||||
|
|
||||||
// 获取起始星球名称(用于报告)
|
// 获取起始星球名称(用于报告)
|
||||||
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
|
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
|
||||||
@@ -854,7 +1000,32 @@
|
|||||||
if (mission.missionType === MissionType.Transport) {
|
if (mission.missionType === MissionType.Transport) {
|
||||||
// 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo)
|
// 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo)
|
||||||
const transportedResources = { ...mission.cargo }
|
const transportedResources = { ...mission.cargo }
|
||||||
|
const isGiftMission = mission.isGift && mission.giftTargetNpcId
|
||||||
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
||||||
|
|
||||||
|
// 更新成就统计(仅在成功时追踪)
|
||||||
|
if (result.success) {
|
||||||
|
const totalTransported =
|
||||||
|
transportedResources.metal + transportedResources.crystal + transportedResources.deuterium + transportedResources.darkMatter
|
||||||
|
if (isGiftMission) {
|
||||||
|
// 送礼成功
|
||||||
|
gameLogic.trackDiplomacyStats(gameStore.player, 'gift', { resourcesAmount: totalTransported })
|
||||||
|
} else {
|
||||||
|
// 普通运输任务成功
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'transport', { resourcesAmount: totalTransported })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成失败原因消息
|
||||||
|
let transportFailMessage = t('missionReports.transportFailed')
|
||||||
|
if (!result.success && result.failReason) {
|
||||||
|
if (result.failReason === 'targetNotFound') {
|
||||||
|
transportFailMessage = t('missionReports.transportFailedTargetNotFound')
|
||||||
|
} else if (result.failReason === 'giftRejected') {
|
||||||
|
transportFailMessage = t('missionReports.transportFailedGiftRejected')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成运输任务报告
|
// 生成运输任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -870,9 +1041,10 @@
|
|||||||
targetPlanetName:
|
targetPlanetName:
|
||||||
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
||||||
success: result.success,
|
success: result.success,
|
||||||
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
|
message: result.success ? t('missionReports.transportSuccess') : transportFailMessage,
|
||||||
details: {
|
details: {
|
||||||
transportedResources
|
transportedResources,
|
||||||
|
failReason: result.failReason
|
||||||
},
|
},
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
@@ -881,6 +1053,13 @@
|
|||||||
if (attackResult) {
|
if (attackResult) {
|
||||||
gameStore.player.battleReports.push(attackResult.battleResult)
|
gameStore.player.battleReports.push(attackResult.battleResult)
|
||||||
|
|
||||||
|
// 更新成就统计 - 攻击
|
||||||
|
const debrisValue = attackResult.debrisField
|
||||||
|
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
|
||||||
|
: 0
|
||||||
|
const won = attackResult.battleResult.winner === 'attacker'
|
||||||
|
gameLogic.trackAttackStats(gameStore.player, attackResult.battleResult, won, debrisValue)
|
||||||
|
|
||||||
// 检查是否攻击了NPC星球,更新外交关系
|
// 检查是否攻击了NPC星球,更新外交关系
|
||||||
if (targetPlanet) {
|
if (targetPlanet) {
|
||||||
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
|
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
|
||||||
@@ -898,7 +1077,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mission.missionType === MissionType.Colonize) {
|
} else if (mission.missionType === MissionType.Colonize) {
|
||||||
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
|
const colonizeResult = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
|
||||||
|
const newPlanet = colonizeResult.planet
|
||||||
|
|
||||||
|
// 更新成就统计 - 殖民
|
||||||
|
if (colonizeResult.success && newPlanet) {
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'colonize')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成失败原因消息
|
||||||
|
let failMessage = t('missionReports.colonizeFailed')
|
||||||
|
if (!colonizeResult.success && colonizeResult.failReason) {
|
||||||
|
if (colonizeResult.failReason === 'positionOccupied') {
|
||||||
|
failMessage = t('missionReports.colonizeFailedOccupied')
|
||||||
|
} else if (colonizeResult.failReason === 'maxColoniesReached') {
|
||||||
|
failMessage = t('missionReports.colonizeFailedMaxColonies')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成殖民任务报告
|
// 生成殖民任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -912,24 +1108,72 @@
|
|||||||
targetPosition: mission.targetPosition,
|
targetPosition: mission.targetPosition,
|
||||||
targetPlanetId: newPlanet?.id,
|
targetPlanetId: newPlanet?.id,
|
||||||
targetPlanetName: newPlanet?.name,
|
targetPlanetName: newPlanet?.name,
|
||||||
success: !!newPlanet,
|
success: colonizeResult.success,
|
||||||
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
|
message: colonizeResult.success ? t('missionReports.colonizeSuccess') : failMessage,
|
||||||
details: newPlanet
|
details: newPlanet
|
||||||
? {
|
? {
|
||||||
newPlanetId: newPlanet.id,
|
newPlanetId: newPlanet.id,
|
||||||
newPlanetName: newPlanet.name
|
newPlanetName: newPlanet.name
|
||||||
}
|
}
|
||||||
: undefined,
|
: { failReason: colonizeResult.failReason },
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
if (newPlanet) {
|
if (newPlanet) {
|
||||||
gameStore.player.planets.push(newPlanet)
|
gameStore.player.planets.push(newPlanet)
|
||||||
}
|
}
|
||||||
} else if (mission.missionType === MissionType.Spy) {
|
} else if (mission.missionType === MissionType.Spy) {
|
||||||
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
|
const spyResult = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
|
||||||
if (spyReport) gameStore.player.spyReports.push(spyReport)
|
if (spyResult.success && spyResult.report) {
|
||||||
|
gameStore.player.spyReports.push(spyResult.report)
|
||||||
|
// 更新成就统计 - 侦查
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'spy')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成侦查任务报告(即使失败也生成)
|
||||||
|
if (!gameStore.player.missionReports) {
|
||||||
|
gameStore.player.missionReports = []
|
||||||
|
}
|
||||||
|
|
||||||
|
let spyFailMessage = t('missionReports.spyFailed')
|
||||||
|
if (!spyResult.success && spyResult.failReason) {
|
||||||
|
if (spyResult.failReason === 'targetNotFound') {
|
||||||
|
spyFailMessage = t('missionReports.spyFailedTargetNotFound')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.player.missionReports.push({
|
||||||
|
id: `mission-report-${mission.id}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
missionType: MissionType.Spy,
|
||||||
|
originPlanetId: mission.originPlanetId,
|
||||||
|
originPlanetName,
|
||||||
|
targetPosition: mission.targetPosition,
|
||||||
|
targetPlanetId: targetPlanet?.id,
|
||||||
|
targetPlanetName:
|
||||||
|
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
||||||
|
success: spyResult.success,
|
||||||
|
message: spyResult.success ? t('missionReports.spySuccess') : spyFailMessage,
|
||||||
|
details: spyResult.success ? { spyReportId: spyResult.report?.id } : { failReason: spyResult.failReason },
|
||||||
|
read: false
|
||||||
|
})
|
||||||
} else if (mission.missionType === MissionType.Deploy) {
|
} else if (mission.missionType === MissionType.Deploy) {
|
||||||
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies)
|
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies)
|
||||||
|
|
||||||
|
// 更新成就统计 - 部署
|
||||||
|
if (deployed.success) {
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'deploy')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成失败原因消息
|
||||||
|
let deployFailMessage = t('missionReports.deployFailed')
|
||||||
|
if (!deployed.success && deployed.failReason) {
|
||||||
|
if (deployed.failReason === 'targetNotFound') {
|
||||||
|
deployFailMessage = t('missionReports.deployFailedTargetNotFound')
|
||||||
|
} else if (deployed.failReason === 'notOwnPlanet') {
|
||||||
|
deployFailMessage = t('missionReports.deployFailedNotOwnPlanet')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成部署任务报告
|
// 生成部署任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -945,9 +1189,10 @@
|
|||||||
targetPlanetName:
|
targetPlanetName:
|
||||||
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
|
||||||
success: deployed.success,
|
success: deployed.success,
|
||||||
message: deployed.success ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
|
message: deployed.success ? t('missionReports.deploySuccess') : deployFailMessage,
|
||||||
details: {
|
details: {
|
||||||
deployedFleet: mission.fleet
|
deployedFleet: mission.fleet,
|
||||||
|
failReason: deployed.failReason
|
||||||
},
|
},
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
@@ -962,6 +1207,23 @@
|
|||||||
const debrisField = universeStore.debrisFields[debrisId]
|
const debrisField = universeStore.debrisFields[debrisId]
|
||||||
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
||||||
|
|
||||||
|
// 更新成就统计 - 回收(无论是否有残骸都算飞行任务,但只有成功回收才计入回收资源量)
|
||||||
|
const totalRecycled =
|
||||||
|
recycleResult.success && recycleResult.collectedResources
|
||||||
|
? recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
|
||||||
|
: 0
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'recycle', { resourcesAmount: totalRecycled })
|
||||||
|
|
||||||
|
// 生成失败原因消息
|
||||||
|
let recycleFailMessage = t('missionReports.recycleFailed')
|
||||||
|
if (!recycleResult.success && recycleResult.failReason) {
|
||||||
|
if (recycleResult.failReason === 'noDebrisField') {
|
||||||
|
recycleFailMessage = t('missionReports.recycleFailedNoDebris')
|
||||||
|
} else if (recycleResult.failReason === 'debrisEmpty') {
|
||||||
|
recycleFailMessage = t('missionReports.recycleFailedDebrisEmpty')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成回收任务报告
|
// 生成回收任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -973,18 +1235,18 @@
|
|||||||
originPlanetId: mission.originPlanetId,
|
originPlanetId: mission.originPlanetId,
|
||||||
originPlanetName,
|
originPlanetName,
|
||||||
targetPosition: mission.targetPosition,
|
targetPosition: mission.targetPosition,
|
||||||
success: !!recycleResult,
|
success: recycleResult.success,
|
||||||
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
|
message: recycleResult.success ? t('missionReports.recycleSuccess') : recycleFailMessage,
|
||||||
details: recycleResult
|
details: recycleResult.success
|
||||||
? {
|
? {
|
||||||
recycledResources: recycleResult.collectedResources,
|
recycledResources: recycleResult.collectedResources,
|
||||||
remainingDebris: recycleResult.remainingDebris || undefined
|
remainingDebris: recycleResult.remainingDebris || undefined
|
||||||
}
|
}
|
||||||
: undefined,
|
: { failReason: recycleResult.failReason },
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (recycleResult && debrisField) {
|
if (recycleResult.success && recycleResult.collectedResources && debrisField) {
|
||||||
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
||||||
// 更新残骸场
|
// 更新残骸场
|
||||||
universeStore.debrisFields[debrisId] = {
|
universeStore.debrisFields[debrisId] = {
|
||||||
@@ -1003,6 +1265,25 @@
|
|||||||
// 处理行星毁灭任务
|
// 处理行星毁灭任务
|
||||||
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
|
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
|
||||||
|
|
||||||
|
// 更新成就统计 - 行星毁灭
|
||||||
|
if (destroyResult.success) {
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'destroy')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成失败原因消息
|
||||||
|
let destroyFailMessage = t('missionReports.destroyFailed')
|
||||||
|
if (!destroyResult.success && destroyResult.failReason) {
|
||||||
|
if (destroyResult.failReason === 'targetNotFound') {
|
||||||
|
destroyFailMessage = t('missionReports.destroyFailedTargetNotFound')
|
||||||
|
} else if (destroyResult.failReason === 'ownPlanet') {
|
||||||
|
destroyFailMessage = t('missionReports.destroyFailedOwnPlanet')
|
||||||
|
} else if (destroyResult.failReason === 'noDeathstar') {
|
||||||
|
destroyFailMessage = t('missionReports.destroyFailedNoDeathstar')
|
||||||
|
} else if (destroyResult.failReason === 'chanceFailed') {
|
||||||
|
destroyFailMessage = t('missionReports.destroyFailedChance', { chance: destroyResult.destructionChance.toFixed(1) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成毁灭任务报告
|
// 生成毁灭任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -1016,19 +1297,23 @@
|
|||||||
targetPosition: mission.targetPosition,
|
targetPosition: mission.targetPosition,
|
||||||
targetPlanetId: targetPlanet?.id,
|
targetPlanetId: targetPlanet?.id,
|
||||||
targetPlanetName: targetPlanet?.name,
|
targetPlanetName: targetPlanet?.name,
|
||||||
success: destroyResult?.success || false,
|
success: destroyResult.success,
|
||||||
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
|
message: destroyResult.success ? t('missionReports.destroySuccess') : destroyFailMessage,
|
||||||
details: destroyResult?.success
|
details: destroyResult.success
|
||||||
? {
|
? {
|
||||||
destroyedPlanetName:
|
destroyedPlanetName:
|
||||||
targetPlanet?.name ||
|
targetPlanet?.name ||
|
||||||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
|
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
|
||||||
}
|
}
|
||||||
: undefined,
|
: {
|
||||||
|
failReason: destroyResult.failReason,
|
||||||
|
destructionChance: destroyResult.destructionChance,
|
||||||
|
deathstarsLost: destroyResult.deathstarsLost
|
||||||
|
},
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
if (destroyResult.success && destroyResult.planetId) {
|
||||||
// 星球被摧毁
|
// 星球被摧毁
|
||||||
|
|
||||||
// 处理外交关系(如果目标是NPC星球)
|
// 处理外交关系(如果目标是NPC星球)
|
||||||
@@ -1036,6 +1321,20 @@
|
|||||||
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
|
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
|
||||||
if (planetOwner) {
|
if (planetOwner) {
|
||||||
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
|
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
|
||||||
|
|
||||||
|
// 从NPC的星球列表中移除被摧毁的星球
|
||||||
|
const npcPlanetIndex = planetOwner.planets.findIndex(p => p.id === destroyResult.planetId)
|
||||||
|
if (npcPlanetIndex > -1) {
|
||||||
|
planetOwner.planets.splice(npcPlanetIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
||||||
|
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
|
||||||
|
|
||||||
|
// 从npcStore中移除被消灭的NPC
|
||||||
|
if (eliminatedNpcIds.length > 0) {
|
||||||
|
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,11 +1346,64 @@
|
|||||||
// 不是玩家星球,从宇宙地图中移除
|
// 不是玩家星球,从宇宙地图中移除
|
||||||
delete universeStore.planets[targetKey]
|
delete universeStore.planets[targetKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消所有前往该位置的NPC任务(回收、攻击、侦查等)
|
||||||
|
const destroyedDebrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
if (npc.fleetMissions) {
|
||||||
|
// 找到需要取消的任务(前往已摧毁星球位置的outbound任务)
|
||||||
|
const missionsToCancel = npc.fleetMissions.filter(m => {
|
||||||
|
if (m.status !== 'outbound') return false
|
||||||
|
// 检查回收任务的残骸场ID
|
||||||
|
if (m.missionType === MissionType.Recycle && m.debrisFieldId === destroyedDebrisId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 检查其他任务的目标星球ID
|
||||||
|
if (m.targetPlanetId === destroyResult.planetId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将这些任务的舰队返回给NPC
|
||||||
|
missionsToCancel.forEach(m => {
|
||||||
|
const npcOriginPlanet = npc.planets.find(p => p.id === m.originPlanetId)
|
||||||
|
if (npcOriginPlanet) {
|
||||||
|
shipLogic.addFleet(npcOriginPlanet.fleet, m.fleet)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从任务列表中移除这些任务
|
||||||
|
npc.fleetMissions = npc.fleetMissions.filter(m => !missionsToCancel.includes(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理关于被摧毁星球的侦查报告
|
||||||
|
if (npc.playerSpyReports && destroyResult.planetId && destroyResult.planetId in npc.playerSpyReports) {
|
||||||
|
delete npc.playerSpyReports[destroyResult.planetId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 同时删除该位置的残骸场(星球被摧毁后残骸场也消失)
|
||||||
|
delete universeStore.debrisFields[destroyedDebrisId]
|
||||||
}
|
}
|
||||||
} else if (mission.missionType === MissionType.Expedition) {
|
} else if (mission.missionType === MissionType.Expedition) {
|
||||||
// 处理远征任务
|
// 处理远征任务
|
||||||
const expeditionResult = fleetLogic.processExpeditionArrival(mission)
|
const expeditionResult = fleetLogic.processExpeditionArrival(mission)
|
||||||
|
|
||||||
|
// 确保返回时间正确设置(兼容旧版本任务数据)
|
||||||
|
// 如果 returnTime 不存在或已过期,重新计算
|
||||||
|
const now = Date.now()
|
||||||
|
if (!mission.returnTime || mission.returnTime <= now) {
|
||||||
|
// 返回时间应该等于当前时间加上单程飞行时间
|
||||||
|
const flightDuration = mission.arrivalTime - mission.departureTime
|
||||||
|
mission.returnTime = now + flightDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新成就统计 - 远征
|
||||||
|
const isSuccessful =
|
||||||
|
expeditionResult.eventType === 'resources' || expeditionResult.eventType === 'darkMatter' || expeditionResult.eventType === 'fleet'
|
||||||
|
gameLogic.trackMissionStats(gameStore.player, 'expedition', { successful: isSuccessful })
|
||||||
|
|
||||||
// 生成远征任务报告
|
// 生成远征任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -1059,32 +1411,35 @@
|
|||||||
|
|
||||||
// 根据事件类型生成不同的报告消息
|
// 根据事件类型生成不同的报告消息
|
||||||
let reportMessage = ''
|
let reportMessage = ''
|
||||||
let reportDetails: Record<string, unknown> = {}
|
let reportDetails: Record<string, unknown> = {
|
||||||
|
// 保存探险区域信息
|
||||||
|
expeditionZone: mission.expeditionZone
|
||||||
|
}
|
||||||
|
|
||||||
switch (expeditionResult.eventType) {
|
switch (expeditionResult.eventType) {
|
||||||
case 'resources':
|
case 'resources':
|
||||||
reportMessage = t('missionReports.expeditionResources')
|
reportMessage = t('missionReports.expeditionResources')
|
||||||
reportDetails = { foundResources: expeditionResult.resources }
|
reportDetails.foundResources = expeditionResult.resources
|
||||||
break
|
break
|
||||||
case 'darkMatter':
|
case 'darkMatter':
|
||||||
reportMessage = t('missionReports.expeditionDarkMatter')
|
reportMessage = t('missionReports.expeditionDarkMatter')
|
||||||
reportDetails = { foundResources: expeditionResult.resources }
|
reportDetails.foundResources = expeditionResult.resources
|
||||||
break
|
break
|
||||||
case 'fleet':
|
case 'fleet':
|
||||||
reportMessage = t('missionReports.expeditionFleet')
|
reportMessage = t('missionReports.expeditionFleet')
|
||||||
reportDetails = { foundFleet: expeditionResult.fleet }
|
reportDetails.foundFleet = expeditionResult.fleet
|
||||||
break
|
break
|
||||||
case 'pirates':
|
case 'pirates':
|
||||||
reportMessage = expeditionResult.fleetLost
|
reportMessage = expeditionResult.fleetLost
|
||||||
? t('missionReports.expeditionPiratesAttack')
|
? t('missionReports.expeditionPiratesAttack')
|
||||||
: t('missionReports.expeditionPiratesEscaped')
|
: t('missionReports.expeditionPiratesEscaped')
|
||||||
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
|
||||||
break
|
break
|
||||||
case 'aliens':
|
case 'aliens':
|
||||||
reportMessage = expeditionResult.fleetLost
|
reportMessage = expeditionResult.fleetLost
|
||||||
? t('missionReports.expeditionAliensAttack')
|
? t('missionReports.expeditionAliensAttack')
|
||||||
: t('missionReports.expeditionAliensEscaped')
|
: t('missionReports.expeditionAliensEscaped')
|
||||||
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
reportMessage = t('missionReports.expeditionNothing')
|
reportMessage = t('missionReports.expeditionNothing')
|
||||||
@@ -1129,7 +1484,13 @@
|
|||||||
const debrisField = universeStore.debrisFields[debrisId]
|
const debrisField = universeStore.debrisFields[debrisId]
|
||||||
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
|
||||||
|
|
||||||
if (recycleResult && debrisField) {
|
if (recycleResult && debrisField && recycleResult.collectedResources) {
|
||||||
|
// 更新成就统计 - 被NPC回收残骸(如果残骸是玩家战斗产生的)
|
||||||
|
const totalRecycled = recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
|
||||||
|
if (totalRecycled > 0) {
|
||||||
|
gameLogic.trackDiplomacyStats(gameStore.player, 'debrisRecycledByNPC', { resourcesAmount: totalRecycled })
|
||||||
|
}
|
||||||
|
|
||||||
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
|
||||||
// 更新残骸场
|
// 更新残骸场
|
||||||
universeStore.debrisFields[debrisId] = {
|
universeStore.debrisFields[debrisId] = {
|
||||||
@@ -1175,6 +1536,9 @@
|
|||||||
// NPC侦查到达
|
// NPC侦查到达
|
||||||
const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player)
|
const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player)
|
||||||
|
|
||||||
|
// 更新成就统计 - 被NPC侦查
|
||||||
|
gameLogic.trackDiplomacyStats(gameStore.player, 'spiedByNPC')
|
||||||
|
|
||||||
// 保存侦查报告到NPC(用于后续攻击决策)
|
// 保存侦查报告到NPC(用于后续攻击决策)
|
||||||
if (!npc.playerSpyReports) {
|
if (!npc.playerSpyReports) {
|
||||||
npc.playerSpyReports = {}
|
npc.playerSpyReports = {}
|
||||||
@@ -1193,6 +1557,14 @@
|
|||||||
// NPC攻击到达 - 使用专门的NPC攻击处理逻辑
|
// NPC攻击到达 - 使用专门的NPC攻击处理逻辑
|
||||||
fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => {
|
fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => {
|
||||||
if (attackResult) {
|
if (attackResult) {
|
||||||
|
// 更新成就统计 - 被NPC攻击 + 防御统计
|
||||||
|
gameLogic.trackDiplomacyStats(gameStore.player, 'attackedByNPC')
|
||||||
|
const debrisValue = attackResult.debrisField
|
||||||
|
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
|
||||||
|
: 0
|
||||||
|
const won = attackResult.battleResult.winner === 'defender'
|
||||||
|
gameLogic.trackDefenseStats(gameStore.player, attackResult.battleResult, won, debrisValue)
|
||||||
|
|
||||||
// 添加战斗报告给玩家
|
// 添加战斗报告给玩家
|
||||||
gameStore.player.battleReports.push(attackResult.battleResult)
|
gameStore.player.battleReports.push(attackResult.battleResult)
|
||||||
|
|
||||||
@@ -1372,6 +1744,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步NPC星球数据到universeStore
|
||||||
|
* 解决npcStore和universeStore数据不同步的问题
|
||||||
|
*/
|
||||||
|
const syncNPCPlanetToUniverse = (npc: any) => {
|
||||||
|
npc.planets.forEach((npcPlanet: any) => {
|
||||||
|
const planetKey = gameLogic.generatePositionKey(npcPlanet.position.galaxy, npcPlanet.position.system, npcPlanet.position.position)
|
||||||
|
const universePlanet = universeStore.planets[planetKey]
|
||||||
|
if (universePlanet) {
|
||||||
|
// 同步所有关键数据
|
||||||
|
universePlanet.resources = { ...npcPlanet.resources }
|
||||||
|
universePlanet.buildings = { ...npcPlanet.buildings }
|
||||||
|
universePlanet.fleet = { ...npcPlanet.fleet }
|
||||||
|
universePlanet.defense = { ...npcPlanet.defense }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const updateNPCGrowth = (deltaSeconds: number) => {
|
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||||
// 累积时间
|
// 累积时间
|
||||||
npcUpdateCounter.value += deltaSeconds
|
npcUpdateCounter.value += deltaSeconds
|
||||||
@@ -1412,7 +1802,7 @@
|
|||||||
|
|
||||||
npcMap.set(planet.ownerId, {
|
npcMap.set(planet.ownerId, {
|
||||||
id: planet.ownerId,
|
id: planet.ownerId,
|
||||||
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
name: generateNPCName(planet.ownerId, gameStore.locale),
|
||||||
planets: [],
|
planets: [],
|
||||||
technologies: {}, // 初始化空科技树
|
technologies: {}, // 初始化空科技树
|
||||||
difficulty: 'medium' as const, // 默认中等难度
|
difficulty: 'medium' as const, // 默认中等难度
|
||||||
@@ -1432,20 +1822,20 @@
|
|||||||
// 保存到store
|
// 保存到store
|
||||||
npcStore.npcs = Array.from(npcMap.values())
|
npcStore.npcs = Array.from(npcMap.values())
|
||||||
|
|
||||||
// 如果有NPC,基于玩家实力初始化NPC
|
// 如果有NPC,基于距离初始化NPC实力
|
||||||
if (npcStore.npcs.length > 0) {
|
if (npcStore.npcs.length > 0) {
|
||||||
const gameState: npcGrowthLogic.NPCGrowthGameState = {
|
// 获取玩家母星(第一个非月球星球)
|
||||||
planets: allPlanets,
|
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
|
||||||
player: gameStore.player,
|
|
||||||
npcs: npcStore.npcs
|
if (homeworld) {
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
// 基于距离初始化NPC实力
|
||||||
|
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
|
||||||
|
// 同步NPC星球数据到universeStore
|
||||||
|
syncNPCPlanetToUniverse(npc)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
|
|
||||||
|
|
||||||
npcStore.npcs.forEach(npc => {
|
|
||||||
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化NPC之间的外交关系(盟友/敌人)
|
// 初始化NPC之间的外交关系(盟友/敌人)
|
||||||
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
|
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
|
||||||
}
|
}
|
||||||
@@ -1456,9 +1846,17 @@
|
|||||||
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保所有NPC都有AI类型(修复旧版本保存的数据)
|
||||||
|
if (npcStore.npcs.length > 0) {
|
||||||
|
npcGrowthLogic.ensureAllNPCsAIType(npcStore.npcs)
|
||||||
|
}
|
||||||
|
|
||||||
// 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据)
|
// 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据)
|
||||||
if (npcStore.npcs.length > 0) {
|
if (npcStore.npcs.length > 0) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
// 获取玩家母星(用于计算距离)
|
||||||
|
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
|
||||||
|
|
||||||
npcStore.npcs.forEach(npc => {
|
npcStore.npcs.forEach(npc => {
|
||||||
if (!npc.relations) {
|
if (!npc.relations) {
|
||||||
npc.relations = {}
|
npc.relations = {}
|
||||||
@@ -1474,6 +1872,19 @@
|
|||||||
history: []
|
history: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 迁移旧存档:如果NPC没有距离数据,计算并设置
|
||||||
|
if (homeworld && npc.distanceToHomeworld === undefined) {
|
||||||
|
const npcPlanet = npc.planets[0]
|
||||||
|
if (npcPlanet) {
|
||||||
|
npc.distanceToHomeworld = npcGrowthLogic.calculateDistanceToHomeworld(npcPlanet.position, homeworld.position)
|
||||||
|
npc.difficultyLevel = npcGrowthLogic.calculateDifficultyLevel(npc.distanceToHomeworld)
|
||||||
|
// 重新初始化NPC实力以匹配新的距离难度系统
|
||||||
|
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
|
||||||
|
// 同步NPC星球数据到universeStore
|
||||||
|
syncNPCPlanetToUniverse(npc)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1483,16 +1894,16 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建游戏状态
|
// 获取玩家母星用于距离计算
|
||||||
const gameState: npcGrowthLogic.NPCGrowthGameState = {
|
const homeworldForGrowth = gameStore.player.planets.find(p => !p.isMoon)
|
||||||
planets: allPlanets,
|
|
||||||
player: gameStore.player,
|
|
||||||
npcs: npcStore.npcs
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用累积的时间更新每个NPC(应用游戏速度倍率)
|
// 使用累积的时间更新每个NPC(基于距离的成长系统)
|
||||||
npcStore.npcs.forEach(npc => {
|
npcStore.npcs.forEach(npc => {
|
||||||
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter.value, gameStore.gameSpeed)
|
if (homeworldForGrowth) {
|
||||||
|
npcGrowthLogic.updateNPCGrowthByDistance(npc, homeworldForGrowth.position, npcUpdateCounter.value, gameStore.gameSpeed)
|
||||||
|
// 同步NPC星球数据到universeStore(确保侦查报告显示正确数据)
|
||||||
|
syncNPCPlanetToUniverse(npc)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重置计数器
|
// 重置计数器
|
||||||
@@ -1566,6 +1977,56 @@
|
|||||||
npcBehaviorCounter.value = 0
|
npcBehaviorCounter.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新NPC关系统计(友好/敌对数量)
|
||||||
|
const updateNPCRelationStats = () => {
|
||||||
|
let friendlyCount = 0
|
||||||
|
let hostileCount = 0
|
||||||
|
const playerId = gameStore.player.id
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
const relation = npc.relations?.[playerId]
|
||||||
|
if (relation) {
|
||||||
|
const status = diplomaticLogic.calculateRelationStatus(relation.reputation)
|
||||||
|
if (status === 'friendly') {
|
||||||
|
friendlyCount++
|
||||||
|
} else if (status === 'hostile') {
|
||||||
|
hostileCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
gameLogic.trackDiplomacyStats(gameStore.player, 'updateRelations', { friendlyCount, hostileCount })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查成就解锁
|
||||||
|
const achievementCheckCounter = ref(0)
|
||||||
|
const ACHIEVEMENT_CHECK_INTERVAL = 5 // 每5秒检查一次成就
|
||||||
|
|
||||||
|
const checkAchievementUnlocks = () => {
|
||||||
|
achievementCheckCounter.value += 1
|
||||||
|
|
||||||
|
// 只在达到更新间隔时才执行
|
||||||
|
if (achievementCheckCounter.value < ACHIEVEMENT_CHECK_INTERVAL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新NPC关系统计
|
||||||
|
updateNPCRelationStats()
|
||||||
|
|
||||||
|
// 检查并解锁成就
|
||||||
|
const unlocks = gameLogic.checkAndUnlockAchievements(gameStore.player)
|
||||||
|
|
||||||
|
// 显示成就解锁通知(奖励已在 checkAndUnlockAchievements 中应用)
|
||||||
|
unlocks.forEach(unlock => {
|
||||||
|
// 显示 toast 通知
|
||||||
|
const tierName = t(`achievements.tiers.${unlock.tier}`)
|
||||||
|
const achievementName = t(`achievements.names.${unlock.id}`)
|
||||||
|
toast.success(t('achievements.unlocked'), {
|
||||||
|
description: `${achievementName} (${tierName})`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
achievementCheckCounter.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 启动游戏循环
|
// 启动游戏循环
|
||||||
const startGameLoop = () => {
|
const startGameLoop = () => {
|
||||||
if (gameStore.isPaused) return
|
if (gameStore.isPaused) return
|
||||||
@@ -1665,45 +2126,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查功能是否解锁
|
// 检查功能是否解锁
|
||||||
const checkFeatureUnlocked = (path: string): { unlocked: boolean; requirement?: { building: BuildingType; level: number } } => {
|
const isFeatureUnlocked = (path: string): boolean => {
|
||||||
const requirement = featureRequirements[path]
|
const requirement = featureRequirements[path]
|
||||||
if (!requirement) {
|
if (!requirement) {
|
||||||
return { unlocked: true }
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLevel = planet.value?.buildings[requirement.building] || 0
|
const currentLevel = planet.value?.buildings[requirement.building] || 0
|
||||||
return {
|
return currentLevel >= requirement.level
|
||||||
unlocked: currentLevel >= requirement.level,
|
|
||||||
requirement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理导航点击
|
|
||||||
const handleNavClick = (path: string, event: Event) => {
|
|
||||||
const { unlocked, requirement } = checkFeatureUnlocked(path)
|
|
||||||
|
|
||||||
if (!unlocked && requirement) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
const buildingName = BUILDINGS.value[requirement.building]?.name || requirement.building
|
|
||||||
const currentLevel = planet.value?.buildings[requirement.building] || 0
|
|
||||||
|
|
||||||
toast.warning(t('common.featureLocked'), {
|
|
||||||
description: `${t('common.requiredBuilding')}: ${buildingName} Lv ${requirement.level} (${t(
|
|
||||||
'common.currentLevel'
|
|
||||||
)}: Lv ${currentLevel})`,
|
|
||||||
action: {
|
|
||||||
label: t('common.goToBuildings'),
|
|
||||||
onClick: () => router.push('/buildings')
|
|
||||||
},
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 功能已解锁,正常导航
|
|
||||||
router.push(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换到月球
|
// 切换到月球
|
||||||
@@ -1820,6 +2249,27 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测旧格式 NPC 名称
|
||||||
|
if (npcStore.npcs.length > 0) {
|
||||||
|
const oldCount = countOldFormatNPCs(npcStore.npcs, gameStore.locale)
|
||||||
|
if (oldCount > 0) {
|
||||||
|
oldFormatNPCCount.value = oldCount
|
||||||
|
npcNameUpdateDialogOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 返回键退出确认
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
|
||||||
|
if (canGoBack) {
|
||||||
|
router.back()
|
||||||
|
} else {
|
||||||
|
exitDialogOpen.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 启动版本检查定时器(每5分钟被动检查一次)
|
// 启动版本检查定时器(每5分钟被动检查一次)
|
||||||
versionCheckInterval.value = setInterval(async () => {
|
versionCheckInterval.value = setInterval(async () => {
|
||||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||||
@@ -1855,7 +2305,35 @@
|
|||||||
// 移除队列取消事件监听
|
// 移除队列取消事件监听
|
||||||
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||||
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||||
|
// 移除 Android 返回键监听
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
CapacitorApp.removeAllListeners()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Android 退出应用
|
||||||
|
const exitApp = () => {
|
||||||
|
CapacitorApp.exitApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPC 名称更新处理
|
||||||
|
const handleUpdateNPCNames = () => {
|
||||||
|
let updatedCount = 0
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
const newName = updateNPCName(npc.id, gameStore.locale)
|
||||||
|
if (newName !== npc.name) {
|
||||||
|
npc.name = newName
|
||||||
|
updatedCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
npcNameUpdateDialogOpen.value = false
|
||||||
|
toast.success(t('settings.npcNameUpdateSuccess', { count: updatedCount }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSkipNPCNameUpdate = () => {
|
||||||
|
npcNameUpdateDialogOpen.value = false
|
||||||
|
toast.info(t('settings.npcNameUpdateSkipped'))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -45,32 +45,7 @@
|
|||||||
--ring: oklch(0.442 0.017 285.786);
|
--ring: oklch(0.442 0.017 285.786);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
/* Theme variables are defined in style.css */
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@@ -93,3 +68,23 @@ html.dark {
|
|||||||
html.light {
|
html.light {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 队列添加动画 - 脉冲效果 */
|
||||||
|
@keyframes queue-pulse-animation {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-pulse {
|
||||||
|
animation: queue-pulse-animation 0.3s ease-out;
|
||||||
|
}
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog v-model:open="isOpen">
|
|
||||||
<ScrollableDialogContent container-class="sm:max-w-4xl max-h-[90vh]">
|
|
||||||
<template #header>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle class="flex items-center gap-2">
|
|
||||||
<Trophy class="h-5 w-5" />
|
|
||||||
{{ t('messagesView.battleReport') }}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription v-if="report">
|
|
||||||
{{ formatDate(report.timestamp) }}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="report" class="space-y-4">
|
|
||||||
<!-- 战斗双方信息 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<!-- 攻击方星球 -->
|
|
||||||
<div class="p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
|
|
||||||
<p class="font-medium text-blue-600 dark:text-blue-400 mb-1">{{ t('simulatorView.attacker') }}</p>
|
|
||||||
<p v-if="attackerPlanet" class="text-xs text-muted-foreground">
|
|
||||||
{{ attackerPlanet.name }} [{{ attackerPlanet.position.galaxy }}:{{ attackerPlanet.position.system }}:{{
|
|
||||||
attackerPlanet.position.position
|
|
||||||
}}]
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-xs text-muted-foreground">{{ report.attackerPlanetId }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 防守方星球 -->
|
|
||||||
<div class="p-3 bg-red-50 dark:bg-red-950/20 rounded-lg">
|
|
||||||
<p class="font-medium text-red-600 dark:text-red-400 mb-1">{{ t('simulatorView.defender') }}</p>
|
|
||||||
<p v-if="defenderPlanet" class="text-xs text-muted-foreground">
|
|
||||||
{{ defenderPlanet.name }} [{{ defenderPlanet.position.galaxy }}:{{ defenderPlanet.position.system }}:{{
|
|
||||||
defenderPlanet.position.position
|
|
||||||
}}]
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-xs text-muted-foreground">{{ report.defenderPlanetId }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 胜利者 -->
|
|
||||||
<div class="text-center p-4 rounded-lg" :class="getPlayerResultStyle()">
|
|
||||||
<p class="text-lg font-bold">
|
|
||||||
{{ report.winner === 'draw' ? t('messagesView.draw') : isPlayerVictory ? t('messagesView.victory') : t('messagesView.defeat') }}
|
|
||||||
</p>
|
|
||||||
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 损失对比 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<!-- 攻击方损失 -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.attackerLosses') }}</p>
|
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
|
||||||
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-muted-foreground">
|
|
||||||
{{ t('messagesView.noLosses') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 防守方损失 -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ t('messagesView.defenderLosses') }}</p>
|
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
|
||||||
<div v-for="(count, shipType) in report.defenderLosses.fleet" :key="shipType">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-for="(count, defenseType) in report.defenderLosses.defense" :key="defenseType">
|
|
||||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
|
|
||||||
class="text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ t('messagesView.noLosses') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 剩余单位 -->
|
|
||||||
<div v-if="hasAnyRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<!-- 攻击方剩余 -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
|
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
|
||||||
<template v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0">
|
|
||||||
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 防守方剩余 -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
|
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
|
||||||
<template
|
|
||||||
v-if="
|
|
||||||
report.defenderRemaining &&
|
|
||||||
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
|
|
||||||
Object.keys(report.defenderRemaining.defense || {}).length > 0)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-for="(count, defenseType) in report.defenderRemaining.defense" :key="defenseType">
|
|
||||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 掠夺资源 -->
|
|
||||||
<div
|
|
||||||
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
|
|
||||||
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
|
||||||
>
|
|
||||||
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
|
|
||||||
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
|
||||||
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="metal" size="sm" />
|
|
||||||
{{ formatNumber(report.plunder.metal) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="report.plunder.crystal > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="crystal" size="sm" />
|
|
||||||
{{ formatNumber(report.plunder.crystal) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="report.plunder.deuterium > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="deuterium" size="sm" />
|
|
||||||
{{ formatNumber(report.plunder.deuterium) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 残骸场 -->
|
|
||||||
<div
|
|
||||||
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
|
|
||||||
class="text-center p-4 bg-muted rounded-lg"
|
|
||||||
>
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
|
|
||||||
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
|
||||||
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="metal" size="sm" />
|
|
||||||
{{ formatNumber(report.debrisField.metal) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="report.debrisField.crystal > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="crystal" size="sm" />
|
|
||||||
{{ formatNumber(report.debrisField.crystal) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- 月球生成概率 -->
|
|
||||||
<p v-if="report.moonChance && report.moonChance > 0" class="text-xs text-muted-foreground mt-2">
|
|
||||||
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 回合详情 -->
|
|
||||||
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
|
|
||||||
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full">
|
|
||||||
{{ showRoundDetails ? t('messagesView.hideRoundDetails') : t('messagesView.showRoundDetails') }}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
|
|
||||||
<!-- 时间线 -->
|
|
||||||
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
|
|
||||||
|
|
||||||
<div v-for="detail in report.roundDetails" :key="detail.round" class="relative">
|
|
||||||
<!-- 时间线节点 -->
|
|
||||||
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background" />
|
|
||||||
|
|
||||||
<!-- 回合内容卡片 -->
|
|
||||||
<div class="border rounded-lg p-3 bg-card hover:shadow-md transition-shadow">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<p class="text-sm font-semibold">{{ t('messagesView.round').replace('{round}', String(detail.round)) }}</p>
|
|
||||||
<TooltipProvider :delay-duration="300">
|
|
||||||
<div class="flex gap-3 text-xs text-muted-foreground">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<Sword class="h-3 w-3" />
|
|
||||||
{{ formatNumber(detail.attackerRemainingPower) }}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{{ t('messagesView.attackerRemainingPower') }}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<Shield class="h-3 w-3" />
|
|
||||||
{{ formatNumber(detail.defenderRemainingPower) }}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{{ t('messagesView.defenderRemainingPower') }}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
||||||
<!-- 攻击方本回合损失 -->
|
|
||||||
<div class="bg-red-50 dark:bg-red-950/20 rounded p-2">
|
|
||||||
<p class="text-xs font-medium text-red-600 dark:text-red-400 mb-1.5">{{ t('messagesView.attackerLosses') }}</p>
|
|
||||||
<div class="text-xs space-y-0.5">
|
|
||||||
<div v-for="(count, shipType) in detail.attackerLosses" :key="shipType" class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
|
||||||
<span class="font-medium">-{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-muted-foreground italic">
|
|
||||||
{{ t('messagesView.noLosses') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 防守方本回合损失 -->
|
|
||||||
<div class="bg-blue-50 dark:bg-blue-950/20 rounded p-2">
|
|
||||||
<p class="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1.5">{{ t('messagesView.defenderLosses') }}</p>
|
|
||||||
<div class="text-xs space-y-0.5">
|
|
||||||
<div v-for="(count, shipType) in detail.defenderLosses.fleet" :key="shipType" class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
|
||||||
<span class="font-medium">-{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-for="(count, defenseType) in detail.defenderLosses.defense" :key="defenseType" class="flex justify-between">
|
|
||||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
|
|
||||||
<span class="font-medium">-{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="
|
|
||||||
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
|
|
||||||
"
|
|
||||||
class="text-muted-foreground italic"
|
|
||||||
>
|
|
||||||
{{ t('messagesView.noLosses') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollableDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch, computed } from 'vue'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useUniverseStore } from '@/stores/universeStore'
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
|
||||||
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
|
||||||
import { formatNumber, formatDate } from '@/utils/format'
|
|
||||||
import { Trophy, Sword, Shield } from 'lucide-vue-next'
|
|
||||||
import type { BattleResult } from '@/types/game'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
report: BattleResult | null
|
|
||||||
open: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:open', value: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const universeStore = useUniverseStore()
|
|
||||||
const { t } = useI18n()
|
|
||||||
const { SHIPS, DEFENSES } = useGameConfig()
|
|
||||||
|
|
||||||
const isOpen = ref(props.open)
|
|
||||||
const showRoundDetails = ref(false)
|
|
||||||
|
|
||||||
// 获取攻击方星球信息
|
|
||||||
const attackerPlanet = computed(() => {
|
|
||||||
if (!props.report) return null
|
|
||||||
// 先从玩家星球中查找
|
|
||||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
|
||||||
if (playerPlanet) return playerPlanet
|
|
||||||
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
|
||||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.attackerPlanetId)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取防守方星球信息
|
|
||||||
const defenderPlanet = computed(() => {
|
|
||||||
if (!props.report) return null
|
|
||||||
// 先从玩家星球中查找
|
|
||||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
|
|
||||||
if (playerPlanet) return playerPlanet
|
|
||||||
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
|
||||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断玩家是攻击方还是防守方
|
|
||||||
const isPlayerAttacker = computed(() => {
|
|
||||||
if (!props.report) return false
|
|
||||||
return gameStore.player.planets.some(p => p.id === props.report!.attackerPlanetId)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断玩家是否胜利
|
|
||||||
const isPlayerVictory = computed(() => {
|
|
||||||
if (!props.report) return false
|
|
||||||
if (props.report.winner === 'draw') return false
|
|
||||||
// 玩家是攻击方且攻击方胜利,或者玩家是防守方且防守方胜利
|
|
||||||
return (isPlayerAttacker.value && props.report.winner === 'attacker') || (!isPlayerAttacker.value && props.report.winner === 'defender')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断是否有任何剩余单位需要显示
|
|
||||||
const hasAnyRemaining = computed(() => {
|
|
||||||
if (!props.report) return false
|
|
||||||
const hasAttackerRemaining = props.report.attackerRemaining && Object.keys(props.report.attackerRemaining).length > 0
|
|
||||||
const hasDefenderRemaining =
|
|
||||||
props.report.defenderRemaining &&
|
|
||||||
(Object.keys(props.report.defenderRemaining.fleet || {}).length > 0 ||
|
|
||||||
Object.keys(props.report.defenderRemaining.defense || {}).length > 0)
|
|
||||||
return hasAttackerRemaining || hasDefenderRemaining
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.open,
|
|
||||||
newValue => {
|
|
||||||
isOpen.value = newValue
|
|
||||||
if (newValue) {
|
|
||||||
showRoundDetails.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(isOpen, newValue => {
|
|
||||||
emit('update:open', newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取玩家战斗结果样式
|
|
||||||
const getPlayerResultStyle = () => {
|
|
||||||
if (!props.report) return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
|
||||||
if (props.report.winner === 'draw') return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
|
||||||
if (isPlayerVictory.value) return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
|
||||||
return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Popover v-model:open="isOpen">
|
|
||||||
<PopoverTrigger as-child>
|
|
||||||
<Button data-tutorial="queue-button" variant="outline" size="icon" class="relative">
|
|
||||||
<ListOrdered class="h-4 w-4" />
|
|
||||||
<Badge
|
|
||||||
v-if="totalQueueCount > 0"
|
|
||||||
variant="default"
|
|
||||||
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
|
|
||||||
>
|
|
||||||
{{ totalQueueCount }}
|
|
||||||
</Badge>
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent class="w-96 p-0" align="end">
|
|
||||||
<div class="flex items-center justify-between p-4 border-b">
|
|
||||||
<h3 class="font-semibold">{{ t('queue.title') }} ({{ totalQueueCount }})</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs v-model="activeTab" class="w-full">
|
|
||||||
<TabsList class="w-full grid grid-cols-5 h-9 rounded-none border-b bg-transparent">
|
|
||||||
<TabsTrigger v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="text-xs px-1 data-[state=active]:bg-muted">
|
|
||||||
{{ t(`queue.tabs.${tab.value}`) }}
|
|
||||||
<Badge v-if="tab.items.length > 0" variant="secondary" class="ml-1 h-4 px-1 text-[10px]">
|
|
||||||
{{ tab.items.length }}
|
|
||||||
</Badge>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<ScrollArea class="h-[420px]">
|
|
||||||
<TabsContent v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="mt-0">
|
|
||||||
<Empty v-if="tab.items.length === 0" class="border-0">
|
|
||||||
<EmptyContent>
|
|
||||||
<Inbox class="h-10 w-10 text-muted-foreground" />
|
|
||||||
<EmptyDescription>{{ t('queue.empty') }}</EmptyDescription>
|
|
||||||
</EmptyContent>
|
|
||||||
</Empty>
|
|
||||||
<div v-else class="divide-y p-4 space-y-3">
|
|
||||||
<div v-for="item in tab.items" :key="item.id" class="space-y-1.5">
|
|
||||||
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
|
||||||
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
|
||||||
<div class="h-2 w-2 rounded-full animate-pulse flex-shrink-0" :class="getStatusDotClass(item)" />
|
|
||||||
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
|
||||||
<span class="text-muted-foreground text-[10px] sm:text-xs">
|
|
||||||
{{
|
|
||||||
item.type === 'ship' || item.type === 'defense'
|
|
||||||
? `→ ${t('queue.quantity')} ${item.quantity}`
|
|
||||||
: item.type === 'demolish'
|
|
||||||
? `→ ${t('queue.demolishing')}`
|
|
||||||
: `→ ${t('queue.level')} ${item.targetLevel}`
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
|
||||||
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
|
|
||||||
{{ formatTime(getRemainingTime(item)) }}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
|
||||||
@click.stop="handleCancel(item)"
|
|
||||||
>
|
|
||||||
{{ t('queue.cancel') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</ScrollArea>
|
|
||||||
</Tabs>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, onUnmounted, watch } from 'vue'
|
|
||||||
import { ListOrdered, Inbox } from 'lucide-vue-next'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Progress } from '@/components/ui/progress'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
||||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
import { formatTime } from '@/utils/format'
|
|
||||||
import type { BuildQueueItem, BuildingType, ShipType, DefenseType, TechnologyType } from '@/types/game'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const gameStore = useGameStore()
|
|
||||||
const { BUILDINGS, SHIPS, DEFENSES, TECHNOLOGIES } = useGameConfig()
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
const activeTab = ref('all')
|
|
||||||
|
|
||||||
// 响应式时间戳,用于驱动时间和进度的动态更新
|
|
||||||
const currentTime = ref(Date.now())
|
|
||||||
let timerInterval: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
// 当弹窗打开时启动计时器,关闭时停止
|
|
||||||
watch(isOpen, open => {
|
|
||||||
if (open) {
|
|
||||||
// 启动每秒更新的计时器
|
|
||||||
timerInterval = setInterval(() => {
|
|
||||||
currentTime.value = Date.now()
|
|
||||||
}, 1000)
|
|
||||||
} else {
|
|
||||||
// 停止计时器
|
|
||||||
if (timerInterval) {
|
|
||||||
clearInterval(timerInterval)
|
|
||||||
timerInterval = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时清理计时器
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timerInterval) {
|
|
||||||
clearInterval(timerInterval)
|
|
||||||
timerInterval = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取当前星球的建造队列
|
|
||||||
const buildQueue = computed(() => {
|
|
||||||
return gameStore.currentPlanet?.buildQueue || []
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取研究队列
|
|
||||||
const researchQueue = computed(() => {
|
|
||||||
return gameStore.player.researchQueue || []
|
|
||||||
})
|
|
||||||
|
|
||||||
// 总队列数量
|
|
||||||
const totalQueueCount = computed(() => {
|
|
||||||
return buildQueue.value.length + researchQueue.value.length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 标签页配置(用于循环渲染)
|
|
||||||
const tabConfig = computed(() => [
|
|
||||||
{ value: 'all', items: [...buildQueue.value, ...researchQueue.value] },
|
|
||||||
{ value: 'buildings', items: buildQueue.value.filter(item => item.type === 'building' || item.type === 'demolish') },
|
|
||||||
{ value: 'research', items: researchQueue.value },
|
|
||||||
{ value: 'ships', items: buildQueue.value.filter(item => item.type === 'ship') },
|
|
||||||
{ value: 'defense', items: buildQueue.value.filter(item => item.type === 'defense') }
|
|
||||||
])
|
|
||||||
|
|
||||||
// 获取队列项名称
|
|
||||||
const getItemName = (item: BuildQueueItem): string => {
|
|
||||||
if (item.type === 'building' || item.type === 'demolish') {
|
|
||||||
return BUILDINGS.value[item.itemType as BuildingType].name
|
|
||||||
} else if (item.type === 'ship') {
|
|
||||||
return SHIPS.value[item.itemType as ShipType].name
|
|
||||||
} else if (item.type === 'defense') {
|
|
||||||
return DEFENSES.value[item.itemType as DefenseType].name
|
|
||||||
} else if (item.type === 'technology') {
|
|
||||||
return TECHNOLOGIES.value[item.itemType as TechnologyType].name
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取剩余时间(使用响应式 currentTime 确保动态更新)
|
|
||||||
const getRemainingTime = (item: BuildQueueItem): number => {
|
|
||||||
return Math.max(0, Math.floor((item.endTime - currentTime.value) / 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取队列进度(使用响应式 currentTime 确保动态更新)
|
|
||||||
const getQueueProgress = (item: BuildQueueItem): number => {
|
|
||||||
const elapsed = currentTime.value - item.startTime
|
|
||||||
const total = item.endTime - item.startTime
|
|
||||||
return Math.min(100, (elapsed / total) * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统一的取消处理
|
|
||||||
const handleCancel = (item: BuildQueueItem) => {
|
|
||||||
let eventName: string
|
|
||||||
if (item.type === 'building' || item.type === 'ship' || item.type === 'defense' || item.type === 'demolish') {
|
|
||||||
eventName = 'cancel-build'
|
|
||||||
} else if (item.type === 'technology') {
|
|
||||||
eventName = 'cancel-research'
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = new CustomEvent(eventName, { detail: item.id })
|
|
||||||
window.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取状态指示点颜色
|
|
||||||
const getStatusDotClass = (item: BuildQueueItem): string => {
|
|
||||||
if (item.type === 'demolish') return 'bg-destructive'
|
|
||||||
if (item.type === 'technology') return 'bg-blue-500'
|
|
||||||
return 'bg-green-500'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog v-model:open="isOpen">
|
|
||||||
<ScrollableDialogContent container-class="sm:max-w-2xl max-h-[90vh]">
|
|
||||||
<template #header>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle class="flex items-center gap-2">
|
|
||||||
<Eye class="h-5 w-5" />
|
|
||||||
{{ t('messagesView.spyReport') }}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription v-if="report">
|
|
||||||
{{ formatDate(report.timestamp) }}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="report" class="space-y-4">
|
|
||||||
<!-- 目标星球信息 -->
|
|
||||||
<div class="p-3 bg-muted rounded-lg">
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.targetPlanet') }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
{{ report.targetPlanetName }} [{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{
|
|
||||||
report.targetPosition.position
|
|
||||||
}}]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 资源 -->
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.resources') }}:</p>
|
|
||||||
<div class="flex flex-wrap gap-3 text-xs sm:text-sm">
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="metal" size="sm" />
|
|
||||||
{{ formatNumber(report.resources.metal) }}
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="crystal" size="sm" />
|
|
||||||
{{ formatNumber(report.resources.crystal) }}
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="deuterium" size="sm" />
|
|
||||||
{{ formatNumber(report.resources.deuterium) }}
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="darkMatter" size="sm" />
|
|
||||||
{{ formatNumber(report.resources.darkMatter) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 舰队(如果有) -->
|
|
||||||
<div v-if="report.fleet && Object.keys(report.fleet).length > 0">
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.fleet') }}:</p>
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
|
|
||||||
<div v-for="(count, shipType) in report.fleet" :key="shipType">
|
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
|
||||||
<span class="ml-1 font-medium">{{ count }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 防御设施(如果有) -->
|
|
||||||
<div v-if="report.defense && hasDefense(report.defense)">
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.defense') }}:</p>
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
|
|
||||||
<div v-for="(count, defenseType) in report.defense" :key="defenseType">
|
|
||||||
<span v-if="count && count > 0" class="block">
|
|
||||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
|
||||||
<span class="ml-1 font-medium">{{ count }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 建筑(如果有) -->
|
|
||||||
<div v-if="report.buildings && Object.keys(report.buildings).length > 0">
|
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.buildings') }}:</p>
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 text-xs sm:text-sm">
|
|
||||||
<div v-for="(level, buildingType) in report.buildings" :key="buildingType">
|
|
||||||
<span class="text-muted-foreground">{{ BUILDINGS[buildingType].name }}:</span>
|
|
||||||
<span class="ml-1 font-medium">Lv.{{ level }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollableDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
|
||||||
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
||||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
|
||||||
import { formatNumber, formatDate } from '@/utils/format'
|
|
||||||
import { Eye } from 'lucide-vue-next'
|
|
||||||
import type { SpyReport } from '@/types/game'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
report: SpyReport | null
|
|
||||||
open: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:open', value: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
|
|
||||||
|
|
||||||
const isOpen = ref(props.open)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.open,
|
|
||||||
newValue => {
|
|
||||||
isOpen.value = newValue
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(isOpen, newValue => {
|
|
||||||
emit('update:open', newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查是否有防御设施
|
|
||||||
const hasDefense = (defense: any): boolean => {
|
|
||||||
if (!defense) return false
|
|
||||||
return Object.values(defense).some((count: any) => count > 0)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
405
src/components/campaign/QuestMap.vue
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<div class="quest-map-container relative">
|
||||||
|
<!-- 可缩放的地图区域 -->
|
||||||
|
<div
|
||||||
|
ref="mapContainer"
|
||||||
|
class="quest-map relative overflow-auto rounded-lg border bg-card/50 backdrop-blur-sm"
|
||||||
|
:style="{ maxHeight: '450px' }"
|
||||||
|
>
|
||||||
|
<!-- 可缩放内容包装器 -->
|
||||||
|
<div
|
||||||
|
class="map-content origin-top-left transition-transform duration-200"
|
||||||
|
:style="{ transform: `scale(${zoomLevel})`, minWidth: calculatedMapWidth + 'px', minHeight: calculatedMapHeight + 'px' }"
|
||||||
|
>
|
||||||
|
<!-- SVG连接线 - 位置与节点容器对齐 -->
|
||||||
|
<svg
|
||||||
|
class="absolute pointer-events-none"
|
||||||
|
:style="{ left: 0, top: 0, width: calculatedMapWidth + 'px', height: calculatedMapHeight + 'px' }"
|
||||||
|
:viewBox="`0 0 ${calculatedMapWidth} ${calculatedMapHeight}`"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<!-- 渐变定义 - 垂直方向 -->
|
||||||
|
<linearGradient id="line-gradient-active" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color: hsl(var(--primary)); stop-opacity: 0.5" />
|
||||||
|
<stop offset="100%" style="stop-color: hsl(var(--primary)); stop-opacity: 1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="line-gradient-locked" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color: hsl(var(--muted-foreground)); stop-opacity: 0.2" />
|
||||||
|
<stop offset="100%" style="stop-color: hsl(var(--muted-foreground)); stop-opacity: 0.3" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="line-gradient-completed" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color: rgb(34, 197, 94); stop-opacity: 0.5" />
|
||||||
|
<stop offset="100%" style="stop-color: rgb(34, 197, 94); stop-opacity: 1" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- 发光滤镜 -->
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 连接线 -->
|
||||||
|
<g>
|
||||||
|
<template v-for="connection in questConnections" :key="connection.id">
|
||||||
|
<path
|
||||||
|
:d="connection.path"
|
||||||
|
fill="none"
|
||||||
|
:stroke="getConnectionStroke(connection)"
|
||||||
|
stroke-width="2"
|
||||||
|
:stroke-dasharray="connection.isLocked ? '5,5' : 'none'"
|
||||||
|
:filter="connection.isActive ? 'url(#glow)' : 'none'"
|
||||||
|
class="transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<!-- 流动动画点(激活状态) -->
|
||||||
|
<circle v-if="connection.isActive" r="3" :fill="'hsl(var(--primary))'" class="animate-flow">
|
||||||
|
<animateMotion :dur="'2s'" repeatCount="indefinite" :path="connection.path" />
|
||||||
|
</circle>
|
||||||
|
</template>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- 任务节点 -->
|
||||||
|
<div class="relative" :style="{ width: calculatedMapWidth + 'px', height: calculatedMapHeight + 'px' }">
|
||||||
|
<div v-for="quest in quests" :key="quest.id" class="absolute transition-all duration-300" :style="getNodeStyle(quest.id)">
|
||||||
|
<QuestNode :quest="quest" :progress="progress" @select="handleQuestSelect" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 地图控制 -->
|
||||||
|
<div class="absolute bottom-4 right-4 flex gap-2">
|
||||||
|
<Button variant="outline" size="icon-sm" @click="zoomIn">
|
||||||
|
<ZoomIn class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="icon-sm" @click="zoomOut">
|
||||||
|
<ZoomOut class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="icon-sm" @click="resetView">
|
||||||
|
<Maximize2 class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图例 -->
|
||||||
|
<div class="absolute top-4 left-4 flex flex-wrap gap-3 text-xs">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-green-500" />
|
||||||
|
<span class="text-muted-foreground">{{ t('campaign.completed') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-primary" />
|
||||||
|
<span class="text-muted-foreground">{{ t('campaign.inProgress') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-blue-400 animate-pulse" />
|
||||||
|
<span class="text-muted-foreground">{{ t('campaign.available') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-muted-foreground/30" />
|
||||||
|
<span class="text-muted-foreground">{{ t('campaign.locked') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { QuestStatus, type CampaignQuestConfig, type PlayerCampaignProgress } from '@/types/game'
|
||||||
|
import { getQuestStatus } from '@/logic/campaignLogic'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import QuestNode from './QuestNode.vue'
|
||||||
|
import { ZoomIn, ZoomOut, Maximize2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
quests: CampaignQuestConfig[]
|
||||||
|
progress: PlayerCampaignProgress | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selectQuest: [questId: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 地图容器引用
|
||||||
|
const mapContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 布局参数 - 从左到右的工作流布局
|
||||||
|
const nodeSize = 56 // 节点实际尺寸 (w-14 = 56px)
|
||||||
|
const nodeRadius = 28 // 节点半径
|
||||||
|
const horizontalGap = 120 // 水平间距(层级之间,包含连线空间)
|
||||||
|
const verticalGap = 40 // 垂直间距(同一层级内)
|
||||||
|
const paddingX = 80
|
||||||
|
const paddingY = 60
|
||||||
|
|
||||||
|
// 缩放级别
|
||||||
|
const zoomLevel = ref(1)
|
||||||
|
|
||||||
|
// 计算工作流布局的节点位置(从左到右)
|
||||||
|
const nodePositions = computed(() => {
|
||||||
|
const positions: Record<string, { x: number; y: number; level: number; index: number }> = {}
|
||||||
|
const levels: Record<number, CampaignQuestConfig[]> = {}
|
||||||
|
|
||||||
|
// 根据任务的依赖关系计算层级
|
||||||
|
const calculateLevel = (quest: CampaignQuestConfig, visited: Set<string> = new Set()): number => {
|
||||||
|
if (visited.has(quest.id)) return 0
|
||||||
|
visited.add(quest.id)
|
||||||
|
|
||||||
|
if (!quest.requiredQuestIds || quest.requiredQuestIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxParentLevel = -1
|
||||||
|
quest.requiredQuestIds.forEach(reqId => {
|
||||||
|
const parentQuest = props.quests.find(q => q.id === reqId)
|
||||||
|
if (parentQuest) {
|
||||||
|
const parentLevel = calculateLevel(parentQuest, visited)
|
||||||
|
maxParentLevel = Math.max(maxParentLevel, parentLevel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return maxParentLevel + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个任务计算层级
|
||||||
|
props.quests.forEach(quest => {
|
||||||
|
const level = calculateLevel(quest)
|
||||||
|
if (!levels[level]) {
|
||||||
|
levels[level] = []
|
||||||
|
}
|
||||||
|
levels[level].push(quest)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按 order 排序每个层级的任务
|
||||||
|
Object.keys(levels).forEach(levelKey => {
|
||||||
|
const level = parseInt(levelKey)
|
||||||
|
const questsAtLevel = levels[level]
|
||||||
|
if (questsAtLevel) {
|
||||||
|
questsAtLevel.sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算每个任务的位置(从左到右布局)
|
||||||
|
const levelKeys = Object.keys(levels)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
|
||||||
|
levelKeys.forEach(level => {
|
||||||
|
const questsInLevel = levels[level]
|
||||||
|
if (!questsInLevel) return
|
||||||
|
|
||||||
|
questsInLevel.forEach((quest, index) => {
|
||||||
|
// 水平位置(层级决定X坐标)
|
||||||
|
const x = paddingX + level * (nodeSize + horizontalGap) + nodeRadius
|
||||||
|
|
||||||
|
// 垂直位置(同层级内的索引决定Y坐标)
|
||||||
|
const startY = paddingY + index * (nodeSize + verticalGap)
|
||||||
|
const y = startY + nodeRadius
|
||||||
|
|
||||||
|
positions[quest.id] = { x, y, level, index }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return positions
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算地图尺寸
|
||||||
|
const calculatedMapWidth = computed(() => {
|
||||||
|
const positions = Object.values(nodePositions.value)
|
||||||
|
if (positions.length === 0) return 400
|
||||||
|
const maxX = Math.max(...positions.map(p => p.x))
|
||||||
|
return Math.max(maxX + paddingX + nodeRadius, 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
const calculatedMapHeight = computed(() => {
|
||||||
|
const positions = Object.values(nodePositions.value)
|
||||||
|
if (positions.length === 0) return 300
|
||||||
|
const maxY = Math.max(...positions.map(p => p.y))
|
||||||
|
return Math.max(maxY + paddingY + nodeRadius, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算连接线
|
||||||
|
interface Connection {
|
||||||
|
id: string
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
path: string
|
||||||
|
isLocked: boolean
|
||||||
|
isActive: boolean
|
||||||
|
isCompleted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const questConnections = computed<Connection[]>(() => {
|
||||||
|
const connections: Connection[] = []
|
||||||
|
|
||||||
|
props.quests.forEach(quest => {
|
||||||
|
if (quest.requiredQuestIds) {
|
||||||
|
quest.requiredQuestIds.forEach(requiredId => {
|
||||||
|
const fromPos = nodePositions.value[requiredId]
|
||||||
|
const toPos = nodePositions.value[quest.id]
|
||||||
|
|
||||||
|
if (fromPos && toPos) {
|
||||||
|
// 从节点右边缘出发,到下一个节点左边缘
|
||||||
|
const startX = fromPos.x + nodeRadius
|
||||||
|
const startY = fromPos.y
|
||||||
|
const endX = toPos.x - nodeRadius
|
||||||
|
const endY = toPos.y
|
||||||
|
|
||||||
|
// 使用水平控制点创建平滑的S型曲线
|
||||||
|
const controlOffset = (endX - startX) / 2
|
||||||
|
const path = `M ${startX} ${startY} C ${startX + controlOffset} ${startY}, ${endX - controlOffset} ${endY}, ${endX} ${endY}`
|
||||||
|
|
||||||
|
// 获取状态
|
||||||
|
const fromQuest = props.quests.find(q => q.id === requiredId)
|
||||||
|
const fromStatus = props.progress && fromQuest ? getQuestStatus(props.progress, fromQuest.id) : QuestStatus.Locked
|
||||||
|
const toStatus = props.progress ? getQuestStatus(props.progress, quest.id) : QuestStatus.Locked
|
||||||
|
|
||||||
|
connections.push({
|
||||||
|
id: `${requiredId}-${quest.id}`,
|
||||||
|
from: requiredId,
|
||||||
|
to: quest.id,
|
||||||
|
path,
|
||||||
|
isLocked: toStatus === QuestStatus.Locked,
|
||||||
|
isActive: toStatus === QuestStatus.Active || toStatus === QuestStatus.Available,
|
||||||
|
isCompleted: fromStatus === QuestStatus.Completed && toStatus === QuestStatus.Completed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return connections
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取连接线颜色
|
||||||
|
const getConnectionStroke = (connection: Connection): string => {
|
||||||
|
if (connection.isCompleted) {
|
||||||
|
return 'rgb(34, 197, 94)'
|
||||||
|
}
|
||||||
|
if (connection.isActive) {
|
||||||
|
return 'hsl(var(--primary))'
|
||||||
|
}
|
||||||
|
// 锁定状态使用更明显的灰色
|
||||||
|
return 'rgba(156, 163, 175, 0.5)'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点样式(处理 undefined 情况)
|
||||||
|
const getNodeStyle = (questId: string) => {
|
||||||
|
const pos = nodePositions.value[questId]
|
||||||
|
if (!pos) {
|
||||||
|
return { left: '0px', top: '0px' }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left: pos.x - nodeRadius + 'px',
|
||||||
|
top: pos.y - nodeRadius + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放控制
|
||||||
|
const zoomIn = () => {
|
||||||
|
zoomLevel.value = Math.min(zoomLevel.value * 1.2, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOut = () => {
|
||||||
|
zoomLevel.value = Math.max(zoomLevel.value / 1.2, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetView = () => {
|
||||||
|
zoomLevel.value = 1
|
||||||
|
if (mapContainer.value) {
|
||||||
|
mapContainer.value.scrollTo({ left: 0, top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理任务选择
|
||||||
|
const handleQuestSelect = (questId: string) => {
|
||||||
|
emit('selectQuest', questId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到当前活动或可用的任务
|
||||||
|
const findActiveQuest = (): string | null => {
|
||||||
|
// 优先找 Active 状态的任务
|
||||||
|
const activeQuest = props.quests.find(quest => {
|
||||||
|
if (!props.progress) return false
|
||||||
|
return getQuestStatus(props.progress, quest.id) === QuestStatus.Active
|
||||||
|
})
|
||||||
|
if (activeQuest) return activeQuest.id
|
||||||
|
|
||||||
|
// 其次找第一个 Available 状态的任务
|
||||||
|
const availableQuest = props.quests.find(quest => {
|
||||||
|
if (!props.progress) return false
|
||||||
|
return getQuestStatus(props.progress, quest.id) === QuestStatus.Available
|
||||||
|
})
|
||||||
|
if (availableQuest) return availableQuest.id
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到指定任务位置(居中显示)
|
||||||
|
const scrollToQuest = (questId: string) => {
|
||||||
|
const container = mapContainer.value
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const pos = nodePositions.value[questId]
|
||||||
|
if (!pos) return
|
||||||
|
|
||||||
|
// 计算需要滚动的位置,使任务节点居中
|
||||||
|
const containerWidth = container.clientWidth
|
||||||
|
const containerHeight = container.clientHeight
|
||||||
|
|
||||||
|
// 考虑缩放比例
|
||||||
|
const scaledX = pos.x * zoomLevel.value
|
||||||
|
const scaledY = pos.y * zoomLevel.value
|
||||||
|
|
||||||
|
// 滚动到节点居中的位置
|
||||||
|
const scrollLeft = Math.max(0, scaledX - containerWidth / 2)
|
||||||
|
const scrollTop = Math.max(0, scaledY - containerHeight / 2)
|
||||||
|
|
||||||
|
container.scrollTo({
|
||||||
|
left: scrollLeft,
|
||||||
|
top: scrollTop,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时滚动到活动任务
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
const activeQuestId = findActiveQuest()
|
||||||
|
if (activeQuestId) {
|
||||||
|
scrollToQuest(activeQuestId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quest-map-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-map {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-flow {
|
||||||
|
filter: drop-shadow(0 0 3px hsl(var(--primary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 星空背景效果 */
|
||||||
|
.quest-map::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 60% 20%, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 100px 100px, 150px 150px, 200px 200px, 120px 120px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
src/components/campaign/QuestNode.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="quest-node"
|
||||||
|
:class="[statusClass, { 'cursor-pointer': isClickable, 'cursor-not-allowed': !isClickable }]"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<!-- 节点主体 -->
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'relative w-14 h-14 rounded-full flex items-center justify-center transition-all duration-300',
|
||||||
|
'border-2 shadow-lg',
|
||||||
|
nodeBackgroundClass
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Boss标记 -->
|
||||||
|
<div v-if="quest.isBoss" class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center">
|
||||||
|
<Skull class="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分支标记 -->
|
||||||
|
<div v-if="quest.isBranch" class="absolute -top-1 -left-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<GitBranch class="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态图标 -->
|
||||||
|
<component :is="statusIcon" :class="['w-6 h-6', iconClass]" />
|
||||||
|
|
||||||
|
<!-- 进度环 -->
|
||||||
|
<svg v-if="status === QuestStatus.Active && progress > 0" class="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 56 56">
|
||||||
|
<circle cx="28" cy="28" r="24" fill="none" stroke="currentColor" stroke-width="3" class="text-primary/30" />
|
||||||
|
<circle
|
||||||
|
cx="28"
|
||||||
|
cy="28"
|
||||||
|
r="24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
class="text-primary"
|
||||||
|
:stroke-dasharray="`${progress * 1.51} 151`"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 节点标题 -->
|
||||||
|
<div class="mt-2 text-center max-w-20">
|
||||||
|
<p :class="['text-xs font-medium truncate', titleClass]">
|
||||||
|
{{ t(quest.titleKey) }}
|
||||||
|
</p>
|
||||||
|
<p v-if="status === QuestStatus.Active" class="text-[10px] text-primary">{{ progress }}%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 脉冲动画(可用状态) -->
|
||||||
|
<div v-if="status === QuestStatus.Available" class="absolute inset-0 w-14 h-14 rounded-full animate-ping bg-blue-400/30" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { QuestStatus, type CampaignQuestConfig, type PlayerCampaignProgress } from '@/types/game'
|
||||||
|
import { getQuestStatus, calculateQuestProgress } from '@/logic/campaignLogic'
|
||||||
|
import { Lock, Circle, CheckCircle2, Play, Skull, GitBranch } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
quest: CampaignQuestConfig
|
||||||
|
progress: PlayerCampaignProgress | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [questId: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 计算任务状态
|
||||||
|
const status = computed(() => {
|
||||||
|
if (!props.progress) return QuestStatus.Locked
|
||||||
|
return getQuestStatus(props.progress, props.quest.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算任务进度百分比
|
||||||
|
const progress = computed(() => {
|
||||||
|
if (!props.progress || status.value !== QuestStatus.Active) return 0
|
||||||
|
return calculateQuestProgress(props.progress, props.quest.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否可点击
|
||||||
|
const isClickable = computed(() => {
|
||||||
|
return status.value !== QuestStatus.Locked
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态样式类
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case QuestStatus.Completed:
|
||||||
|
return 'quest-completed'
|
||||||
|
case QuestStatus.Active:
|
||||||
|
return 'quest-active'
|
||||||
|
case QuestStatus.Available:
|
||||||
|
return 'quest-available'
|
||||||
|
default:
|
||||||
|
return 'quest-locked'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 节点背景样式
|
||||||
|
const nodeBackgroundClass = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case QuestStatus.Completed:
|
||||||
|
return 'bg-green-500/20 border-green-500'
|
||||||
|
case QuestStatus.Active:
|
||||||
|
return 'bg-primary/20 border-primary'
|
||||||
|
case QuestStatus.Available:
|
||||||
|
return 'bg-blue-500/10 border-blue-400 hover:border-blue-300 hover:bg-blue-500/20'
|
||||||
|
default:
|
||||||
|
return 'bg-muted/50 border-muted-foreground/30'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态图标
|
||||||
|
const statusIcon = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case QuestStatus.Completed:
|
||||||
|
return CheckCircle2
|
||||||
|
case QuestStatus.Active:
|
||||||
|
return Play
|
||||||
|
case QuestStatus.Available:
|
||||||
|
return Circle
|
||||||
|
default:
|
||||||
|
return Lock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图标样式
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case QuestStatus.Completed:
|
||||||
|
return 'text-green-500'
|
||||||
|
case QuestStatus.Active:
|
||||||
|
return 'text-primary'
|
||||||
|
case QuestStatus.Available:
|
||||||
|
return 'text-blue-400'
|
||||||
|
default:
|
||||||
|
return 'text-muted-foreground/50'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标题样式
|
||||||
|
const titleClass = computed(() => {
|
||||||
|
switch (status.value) {
|
||||||
|
case QuestStatus.Completed:
|
||||||
|
return 'text-green-500'
|
||||||
|
case QuestStatus.Active:
|
||||||
|
return 'text-primary'
|
||||||
|
case QuestStatus.Available:
|
||||||
|
return 'text-foreground'
|
||||||
|
default:
|
||||||
|
return 'text-muted-foreground/50'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理点击
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isClickable.value) {
|
||||||
|
emit('select', props.quest.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quest-node {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-available .quest-node:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 5px 2px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 15px 5px rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-available > div:first-child {
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
271
src/components/campaign/StoryDialog.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :open="true" @update:open="handleClose">
|
||||||
|
<DialogContent class="max-w-2xl p-0 overflow-hidden bg-gradient-to-b from-background to-background/95">
|
||||||
|
<!-- 对话框头部 - 星空效果 -->
|
||||||
|
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
|
<div class="stars-bg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话内容区 -->
|
||||||
|
<div class="relative p-6 min-h-[300px] flex flex-col">
|
||||||
|
<!-- 说话者信息 -->
|
||||||
|
<div v-if="currentDialogue" class="mb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- 头像 -->
|
||||||
|
<div :class="['w-12 h-12 rounded-full flex items-center justify-center', speakerStyles.bg]">
|
||||||
|
<component :is="speakerIcon" :class="['w-6 h-6', speakerStyles.text]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说话者名称 -->
|
||||||
|
<div>
|
||||||
|
<span :class="['font-semibold', speakerStyles.text]">
|
||||||
|
{{ getSpeakerName(currentDialogue) }}
|
||||||
|
</span>
|
||||||
|
<div v-if="currentDialogue.speaker === 'mysterious'" class="text-xs text-muted-foreground">
|
||||||
|
{{ t('campaign.dialogue.unknownSource') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对话文本 - 打字机效果 -->
|
||||||
|
<div class="flex-1 min-h-[120px]">
|
||||||
|
<div class="text-base leading-relaxed">
|
||||||
|
<span>{{ displayedText }}</span>
|
||||||
|
<span v-if="isTyping" class="animate-pulse">▊</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选项按钮 -->
|
||||||
|
<div v-if="showChoices && currentDialogue?.choices" class="space-y-2 mt-4">
|
||||||
|
<Button
|
||||||
|
v-for="(choice, index) in currentDialogue.choices"
|
||||||
|
:key="index"
|
||||||
|
variant="outline"
|
||||||
|
class="w-full justify-start text-left h-auto py-3 px-4"
|
||||||
|
@click="handleChoice(choice)"
|
||||||
|
>
|
||||||
|
<ChevronRight class="w-4 h-4 mr-2 shrink-0" />
|
||||||
|
<span>{{ t(choice.textKey) }}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 继续按钮 -->
|
||||||
|
<div v-if="!showChoices" class="mt-4 flex justify-end gap-2">
|
||||||
|
<Button v-if="isTyping" variant="ghost" size="sm" @click="skipTyping">
|
||||||
|
{{ t('campaign.dialogue.skip') }}
|
||||||
|
</Button>
|
||||||
|
<Button v-else @click="handleContinue" :disabled="isTyping">
|
||||||
|
{{ isLastDialogue ? t('campaign.dialogue.finish') : t('campaign.dialogue.continue') }}
|
||||||
|
<ChevronRight class="w-4 h-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度指示器 -->
|
||||||
|
<div class="mt-4 flex justify-center gap-1">
|
||||||
|
<div
|
||||||
|
v-for="(_, index) in dialogues"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'w-2 h-2 rounded-full transition-all',
|
||||||
|
index < currentIndex ? 'bg-primary' : index === currentIndex ? 'bg-primary/50' : 'bg-muted'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import type { StoryDialogue, DialogueChoice } from '@/types/game'
|
||||||
|
import { User, Bot, HelpCircle, MessageCircle, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dialogues: StoryDialogue[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
choice: [choice: DialogueChoice]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 当前对话索引
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
|
||||||
|
// 打字机效果状态
|
||||||
|
const displayedText = ref('')
|
||||||
|
const isTyping = ref(false)
|
||||||
|
const typewriterInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
// 当前对话
|
||||||
|
const currentDialogue = computed(() => props.dialogues[currentIndex.value])
|
||||||
|
|
||||||
|
// 是否是最后一个对话
|
||||||
|
const isLastDialogue = computed(() => currentIndex.value >= props.dialogues.length - 1)
|
||||||
|
|
||||||
|
// 是否显示选项
|
||||||
|
const showChoices = computed(() => {
|
||||||
|
return !isTyping.value && currentDialogue.value?.choices && currentDialogue.value.choices.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 说话者图标
|
||||||
|
const speakerIcon = computed(() => {
|
||||||
|
switch (currentDialogue.value?.speaker) {
|
||||||
|
case 'player':
|
||||||
|
return User
|
||||||
|
case 'npc':
|
||||||
|
return Bot
|
||||||
|
case 'mysterious':
|
||||||
|
return HelpCircle
|
||||||
|
default:
|
||||||
|
return MessageCircle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 说话者样式
|
||||||
|
const speakerStyles = computed(() => {
|
||||||
|
switch (currentDialogue.value?.speaker) {
|
||||||
|
case 'player':
|
||||||
|
return { bg: 'bg-blue-500/20', text: 'text-blue-400' }
|
||||||
|
case 'npc':
|
||||||
|
return { bg: 'bg-green-500/20', text: 'text-green-400' }
|
||||||
|
case 'mysterious':
|
||||||
|
return { bg: 'bg-purple-500/20', text: 'text-purple-400' }
|
||||||
|
default:
|
||||||
|
return { bg: 'bg-muted', text: 'text-muted-foreground' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取说话者名称
|
||||||
|
const getSpeakerName = (dialogue: StoryDialogue): string => {
|
||||||
|
if (dialogue.speakerNameKey) {
|
||||||
|
return t(dialogue.speakerNameKey)
|
||||||
|
}
|
||||||
|
switch (dialogue.speaker) {
|
||||||
|
case 'player':
|
||||||
|
return t('campaign.dialogue.player')
|
||||||
|
case 'npc':
|
||||||
|
return t('campaign.dialogue.npc')
|
||||||
|
case 'mysterious':
|
||||||
|
return t('campaign.dialogue.mysterious')
|
||||||
|
default:
|
||||||
|
return t('campaign.dialogue.narrator')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始打字机效果
|
||||||
|
const startTypewriter = () => {
|
||||||
|
const text = t(currentDialogue.value?.textKey || '')
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
displayedText.value = ''
|
||||||
|
isTyping.value = true
|
||||||
|
let charIndex = 0
|
||||||
|
|
||||||
|
typewriterInterval.value = setInterval(() => {
|
||||||
|
if (charIndex < text.length) {
|
||||||
|
displayedText.value += text[charIndex]
|
||||||
|
charIndex++
|
||||||
|
} else {
|
||||||
|
stopTypewriter()
|
||||||
|
}
|
||||||
|
}, 30) // 每30ms显示一个字符
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止打字机效果
|
||||||
|
const stopTypewriter = () => {
|
||||||
|
if (typewriterInterval.value) {
|
||||||
|
clearInterval(typewriterInterval.value)
|
||||||
|
typewriterInterval.value = null
|
||||||
|
}
|
||||||
|
isTyping.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过打字机效果
|
||||||
|
const skipTyping = () => {
|
||||||
|
stopTypewriter()
|
||||||
|
displayedText.value = t(currentDialogue.value?.textKey || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理继续
|
||||||
|
const handleContinue = () => {
|
||||||
|
if (isLastDialogue.value) {
|
||||||
|
emit('close')
|
||||||
|
} else {
|
||||||
|
currentIndex.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选项选择
|
||||||
|
const handleChoice = (choice: DialogueChoice) => {
|
||||||
|
emit('choice', choice)
|
||||||
|
|
||||||
|
// 如果有下一个对话ID,跳转到对应对话
|
||||||
|
if (choice.nextDialogueId) {
|
||||||
|
const nextIndex = props.dialogues.findIndex(d => d.id === choice.nextDialogueId)
|
||||||
|
if (nextIndex !== -1) {
|
||||||
|
currentIndex.value = nextIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则继续到下一个
|
||||||
|
handleContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理关闭
|
||||||
|
const handleClose = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听对话变化,启动打字机
|
||||||
|
watch(
|
||||||
|
currentIndex,
|
||||||
|
() => {
|
||||||
|
stopTypewriter()
|
||||||
|
startTypewriter()
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
startTypewriter()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTypewriter()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stars-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.02) 2px, transparent 2px);
|
||||||
|
background-size: 50px 50px, 80px 80px, 120px 120px;
|
||||||
|
animation: twinkle 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!isUnlocked" class="absolute inset-0 z-10 bg-background/70 backdrop-blur-[2px] rounded-lg flex items-center justify-center">
|
<!-- 遮罩从标题下方开始,不遮挡名称 -->
|
||||||
|
<div
|
||||||
|
v-if="!isUnlocked"
|
||||||
|
class="absolute inset-x-0 top-30 sm:top-25 bottom-0 z-10 bg-background/70 backdrop-blur-[2px] rounded-b-lg flex items-center justify-center"
|
||||||
|
>
|
||||||
<div class="text-center p-4 space-y-2">
|
<div class="text-center p-4 space-y-2">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="rounded-full bg-muted p-2">
|
<div class="rounded-full bg-muted p-2">
|
||||||
@@ -20,8 +24,8 @@
|
|||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="(req, index) in requirementsDialogItems" :key="index" class="flex items-center gap-2 text-sm">
|
<div v-for="(req, index) in requirementsDialogItems" :key="index" class="flex items-center gap-2 text-sm">
|
||||||
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
<Check v-if="req.met" :size="16" class="text-green-500 shrink-0" />
|
||||||
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
<X v-else :size="16" class="text-red-500 shrink-0" />
|
||||||
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 建筑/科技:等级范围表格 -->
|
<!-- 建筑/科技:等级范围表格 -->
|
||||||
<div v-if="type === 'building' || type === 'technology'" class="border rounded-lg overflow-hidden">
|
<div v-if="type === 'building' || type === 'technology'" class="border rounded-lg">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -166,6 +166,56 @@
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 矿脉储量信息(仅采矿建筑显示) -->
|
||||||
|
<Card
|
||||||
|
v-if="isMiningBuilding && oreDepositInfo && miningResourceType"
|
||||||
|
class="border-2"
|
||||||
|
:class="oreDepositInfo.isDepleted ? 'border-destructive' : oreDepositInfo.isWarning ? 'border-yellow-500' : 'border-primary/30'"
|
||||||
|
>
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
|
<ResourceIcon :type="miningResourceType" size="md" />
|
||||||
|
{{ t('buildings.oreDeposit') }}
|
||||||
|
<AlertTriangle
|
||||||
|
v-if="oreDepositInfo.isWarning || oreDepositInfo.isDepleted"
|
||||||
|
class="h-4 w-4"
|
||||||
|
:class="oreDepositInfo.isDepleted ? 'text-destructive' : 'text-yellow-500'"
|
||||||
|
/>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-muted-foreground">{{ t('buildings.remainingDeposit') }}:</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="oreDepositInfo.remaining" />
|
||||||
|
/
|
||||||
|
<NumberWithTooltip :value="oreDepositInfo.initial" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
:model-value="oreDepositInfo.percentage"
|
||||||
|
class="h-2"
|
||||||
|
:class="oreDepositInfo.isDepleted ? 'bg-destructive/20' : oreDepositInfo.isWarning ? 'bg-yellow-500/20' : ''"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<span>{{ oreDepositInfo.percentage.toFixed(1) }}%</span>
|
||||||
|
<span v-if="!oreDepositInfo.isDepleted">{{ t('buildings.depletionTime') }}: {{ oreDepositInfo.depletionTimeFormatted }}</span>
|
||||||
|
<span v-else class="text-destructive font-medium">{{ t('buildings.depositDepleted') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="oreDepositInfo.isWarning && !oreDepositInfo.isDepleted"
|
||||||
|
class="text-xs text-yellow-600 dark:text-yellow-400 bg-yellow-500/10 p-2 rounded"
|
||||||
|
>
|
||||||
|
{{ t('buildings.depositWarning') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="oreDepositInfo.isDepleted" class="text-xs text-destructive bg-destructive/10 p-2 rounded">
|
||||||
|
{{ t('buildings.depositDepletedMessage') }}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 建筑/科技:累积统计 -->
|
<!-- 建筑/科技:累积统计 -->
|
||||||
<div v-if="type === 'building' || type === 'technology'" class="grid grid-cols-2 gap-4">
|
<div v-if="type === 'building' || type === 'technology'" class="grid grid-cols-2 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -393,15 +443,20 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import NumberWithTooltip from '@/components/NumberWithTooltip.vue'
|
import NumberWithTooltip from '@/components/common/NumberWithTooltip.vue'
|
||||||
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
|
import { Sword, Shield, ShieldCheck, Zap, Package, Fuel } from 'lucide-vue-next'
|
||||||
import * as buildingLogic from '@/logic/buildingLogic'
|
import * as buildingLogic from '@/logic/buildingLogic'
|
||||||
import * as researchLogic from '@/logic/researchLogic'
|
import * as researchLogic from '@/logic/researchLogic'
|
||||||
import * as pointsLogic from '@/logic/pointsLogic'
|
import * as pointsLogic from '@/logic/pointsLogic'
|
||||||
import * as officerLogic from '@/logic/officerLogic'
|
import * as officerLogic from '@/logic/officerLogic'
|
||||||
import * as shipLogic from '@/logic/shipLogic'
|
import * as shipLogic from '@/logic/shipLogic'
|
||||||
|
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
||||||
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
import { SHIPS, DEFENSES } from '@/config/gameConfig'
|
import { SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||||
import { formatTime } from '@/utils/format'
|
import { formatTime } from '@/utils/format'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
|
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||||
|
import { AlertTriangle } from 'lucide-vue-next'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
@@ -442,6 +497,11 @@
|
|||||||
return currentPlanet.value.buildings['researchLab'] || 0
|
return currentPlanet.value.buildings['researchLab'] || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取能量科技等级(用于研究时间计算)
|
||||||
|
const energyTechLevel = computed(() => {
|
||||||
|
return gameStore.player.technologies['energyTechnology'] || 0
|
||||||
|
})
|
||||||
|
|
||||||
// 翻译键(转换为复数形式)
|
// 翻译键(转换为复数形式)
|
||||||
const typeKey = computed(() => {
|
const typeKey = computed(() => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
@@ -472,11 +532,79 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showConsumptionColumn = computed(() => {
|
const showConsumptionColumn = computed(() => {
|
||||||
|
if (props.type !== 'building') return false
|
||||||
|
const buildingType = props.itemType as BuildingType
|
||||||
|
// 所有消耗电力的建筑
|
||||||
|
return [
|
||||||
|
'metalMine',
|
||||||
|
'crystalMine',
|
||||||
|
'deuteriumSynthesizer',
|
||||||
|
'roboticsFactory',
|
||||||
|
'naniteFactory',
|
||||||
|
'shipyard',
|
||||||
|
'researchLab',
|
||||||
|
'missileSilo',
|
||||||
|
'terraformer',
|
||||||
|
'darkMatterCollector',
|
||||||
|
'sensorPhalanx',
|
||||||
|
'jumpGate'
|
||||||
|
].includes(buildingType)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示矿脉储量信息(仅对采矿建筑)
|
||||||
|
const isMiningBuilding = computed(() => {
|
||||||
if (props.type !== 'building') return false
|
if (props.type !== 'building') return false
|
||||||
const buildingType = props.itemType as BuildingType
|
const buildingType = props.itemType as BuildingType
|
||||||
return ['metalMine', 'crystalMine', 'deuteriumSynthesizer'].includes(buildingType)
|
return ['metalMine', 'crystalMine', 'deuteriumSynthesizer'].includes(buildingType)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取当前建筑对应的资源类型
|
||||||
|
const miningResourceType = computed((): 'metal' | 'crystal' | 'deuterium' | null => {
|
||||||
|
if (!isMiningBuilding.value) return null
|
||||||
|
const buildingType = props.itemType as BuildingType
|
||||||
|
if (buildingType === 'metalMine') return 'metal'
|
||||||
|
if (buildingType === 'crystalMine') return 'crystal'
|
||||||
|
if (buildingType === 'deuteriumSynthesizer') return 'deuterium'
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 矿脉储量信息
|
||||||
|
const oreDepositInfo = computed(() => {
|
||||||
|
if (!currentPlanet.value || !miningResourceType.value || !currentPlanet.value.oreDeposits) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const deposits = currentPlanet.value.oreDeposits
|
||||||
|
const resourceType = miningResourceType.value
|
||||||
|
const remaining = deposits[resourceType]
|
||||||
|
const initial =
|
||||||
|
resourceType === 'metal' ? deposits.initialMetal : resourceType === 'crystal' ? deposits.initialCrystal : deposits.initialDeuterium
|
||||||
|
const percentage = oreDepositLogic.getDepositPercentage(deposits, resourceType)
|
||||||
|
const isWarning = oreDepositLogic.isDepositWarning(deposits, resourceType)
|
||||||
|
const isDepleted = oreDepositLogic.isDepositDepleted(deposits, resourceType)
|
||||||
|
|
||||||
|
// 计算当前产量(每小时)
|
||||||
|
const production = resourceLogic.calculateResourceProduction(currentPlanet.value, {
|
||||||
|
resourceProductionBonus: activeBonuses.value.resourceProductionBonus,
|
||||||
|
darkMatterProductionBonus: activeBonuses.value.darkMatterProductionBonus,
|
||||||
|
energyProductionBonus: activeBonuses.value.energyProductionBonus
|
||||||
|
})
|
||||||
|
const productionPerHour = production[resourceType]
|
||||||
|
|
||||||
|
// 计算耗尽时间
|
||||||
|
const depletionTimeHours = oreDepositLogic.calculateDepletionTime(deposits, resourceType, productionPerHour)
|
||||||
|
const depletionTimeFormatted = oreDepositLogic.formatDepletionTime(depletionTimeHours)
|
||||||
|
|
||||||
|
return {
|
||||||
|
remaining,
|
||||||
|
initial,
|
||||||
|
percentage,
|
||||||
|
isWarning,
|
||||||
|
isDepleted,
|
||||||
|
productionPerHour,
|
||||||
|
depletionTimeFormatted
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const showCapacityColumn = computed(() => {
|
const showCapacityColumn = computed(() => {
|
||||||
if (props.type !== 'building') return false
|
if (props.type !== 'building') return false
|
||||||
const buildingType = props.itemType as BuildingType
|
const buildingType = props.itemType as BuildingType
|
||||||
@@ -717,7 +845,8 @@
|
|||||||
}),
|
}),
|
||||||
darkMatterCollector: lvl => ({
|
darkMatterCollector: lvl => ({
|
||||||
capacity: 1000 + lvl * 100,
|
capacity: 1000 + lvl * 100,
|
||||||
production: Math.floor(25 * lvl * Math.pow(1.5, lvl))
|
production: Math.floor(25 * lvl * Math.pow(1.5, lvl)),
|
||||||
|
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
|
||||||
}),
|
}),
|
||||||
darkMatterTank: lvl => ({
|
darkMatterTank: lvl => ({
|
||||||
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
|
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
|
||||||
@@ -726,25 +855,39 @@
|
|||||||
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
|
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
|
||||||
}),
|
}),
|
||||||
shipyard: lvl => ({
|
shipyard: lvl => ({
|
||||||
fleetStorage: 1000 * lvl
|
fleetStorage: 1000 * lvl,
|
||||||
|
consumption: Math.floor(8 * lvl * Math.pow(1.1, lvl))
|
||||||
}),
|
}),
|
||||||
hangar: lvl => ({
|
hangar: lvl => ({
|
||||||
fleetStorage: 500 * lvl
|
fleetStorage: 500 * lvl
|
||||||
}),
|
}),
|
||||||
terraformer: () => ({
|
|
||||||
spaceBonus: 30
|
|
||||||
}),
|
|
||||||
lunarBase: () => ({
|
lunarBase: () => ({
|
||||||
spaceBonus: 30
|
spaceBonus: 30
|
||||||
}),
|
}),
|
||||||
roboticsFactory: lvl => ({
|
roboticsFactory: lvl => ({
|
||||||
buildSpeedBonus: lvl
|
buildSpeedBonus: lvl,
|
||||||
|
consumption: Math.floor(5 * lvl * Math.pow(1.1, lvl))
|
||||||
}),
|
}),
|
||||||
naniteFactory: lvl => ({
|
naniteFactory: lvl => ({
|
||||||
buildSpeedBonus: lvl * 2
|
buildSpeedBonus: lvl * 2,
|
||||||
|
consumption: Math.floor(20 * lvl * Math.pow(1.15, lvl))
|
||||||
}),
|
}),
|
||||||
researchLab: lvl => ({
|
researchLab: lvl => ({
|
||||||
researchSpeedBonus: lvl
|
researchSpeedBonus: lvl,
|
||||||
|
consumption: Math.floor(12 * lvl * Math.pow(1.1, lvl))
|
||||||
|
}),
|
||||||
|
missileSilo: lvl => ({
|
||||||
|
consumption: Math.floor(8 * lvl * Math.pow(1.1, lvl))
|
||||||
|
}),
|
||||||
|
terraformer: lvl => ({
|
||||||
|
spaceBonus: 30,
|
||||||
|
consumption: Math.floor(25 * lvl * Math.pow(1.15, lvl))
|
||||||
|
}),
|
||||||
|
sensorPhalanx: lvl => ({
|
||||||
|
consumption: Math.floor(15 * lvl * Math.pow(1.12, lvl))
|
||||||
|
}),
|
||||||
|
jumpGate: lvl => ({
|
||||||
|
consumption: Math.floor(50 * lvl * Math.pow(1.2, lvl))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,7 +915,8 @@
|
|||||||
techType,
|
techType,
|
||||||
level - 1,
|
level - 1,
|
||||||
activeBonuses.value.researchSpeedBonus,
|
activeBonuses.value.researchSpeedBonus,
|
||||||
researchLabLevel.value
|
researchLabLevel.value,
|
||||||
|
energyTechLevel.value
|
||||||
)
|
)
|
||||||
|
|
||||||
let researchSpeedBonus = 0
|
let researchSpeedBonus = 0
|
||||||
@@ -3,23 +3,17 @@
|
|||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ formatNumber(value, 1) }}</span>
|
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ formatNumber(value, 1) }}</span>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-auto p-2" side="top" align="center">
|
<PopoverContent class="w-auto p-2 z-100" side="top" align="center">
|
||||||
<p class="font-mono text-sm">{{ formattedValue }}</p>
|
<p class="font-mono text-sm">{{ props.value.toLocaleString() }}</p>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
value: number
|
value: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 完整格式化的数字(带千位分隔符)
|
|
||||||
const formattedValue = computed(() => {
|
|
||||||
return props.value.toLocaleString()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
628
src/components/dialogs/BattleAnimationPlayer.vue
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
<template>
|
||||||
|
<div class="battle-animation-container">
|
||||||
|
<!-- 播放控制栏 -->
|
||||||
|
<div class="flex items-center justify-between gap-2 mb-4 p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="icon" @click="restart" :disabled="!canRestart">
|
||||||
|
<RotateCcw class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="icon" @click="previousRound" :disabled="!canGoPrevious">
|
||||||
|
<SkipBack class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button :variant="isPlaying ? 'default' : 'outline'" size="icon" @click="togglePlay" :disabled="!canPlay">
|
||||||
|
<Pause v-if="isPlaying" class="h-4 w-4" />
|
||||||
|
<Play v-else class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="icon" @click="nextRound" :disabled="!canGoNext">
|
||||||
|
<SkipForward class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 播放速度 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('messagesView.speed') }}:</span>
|
||||||
|
<Select v-model="speedMultiplier">
|
||||||
|
<SelectTrigger class="w-20 h-8">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent class="z-100">
|
||||||
|
<SelectItem value="0.5">0.5x</SelectItem>
|
||||||
|
<SelectItem value="1">1x</SelectItem>
|
||||||
|
<SelectItem value="2">2x</SelectItem>
|
||||||
|
<SelectItem value="4">4x</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 战斗场景 -->
|
||||||
|
<div class="battle-scene relative overflow-hidden rounded-lg border bg-gradient-to-b from-slate-900 to-slate-950 p-4 min-h-[300px]">
|
||||||
|
<!-- 星空背景 -->
|
||||||
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div v-for="i in 20" :key="i" class="star" :style="getStarStyle(i)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 攻击方区域 -->
|
||||||
|
<div class="relative z-10 flex justify-between items-center gap-4">
|
||||||
|
<!-- 攻击方舰队 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Sword class="h-4 w-4 text-red-400" />
|
||||||
|
<span class="text-sm font-medium text-red-400">{{ t('simulatorView.attacker') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="fleet-display attacker" :class="{ attacking: attackAnimationPhase === 'attack' }">
|
||||||
|
<div class="grid grid-cols-5 gap-1">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in currentAttackerFleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="ship-unit flex flex-col items-center p-1.5 rounded bg-red-950/50 border border-red-900/50"
|
||||||
|
:class="{ 'exploding': isShipExploding('attacker', shipType as ShipType) }"
|
||||||
|
>
|
||||||
|
<Rocket class="h-4 w-4 text-red-400" />
|
||||||
|
<span class="text-[10px] text-red-300">{{ formatNumber(count, 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-xs text-red-400/80">{{ t('messagesView.power') }}: {{ formatNumber(currentAttackerPower) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS 标志 -->
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="vs-badge w-12 h-12 rounded-full bg-yellow-500/20 border-2 border-yellow-500/50 flex items-center justify-center"
|
||||||
|
:class="{ 'pulse-animation': attackAnimationPhase === 'attack' }"
|
||||||
|
>
|
||||||
|
<Swords class="h-6 w-6 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<!-- 当前回合损失动画 -->
|
||||||
|
<Transition name="damage-popup">
|
||||||
|
<div v-if="showDamageNumbers" class="damage-numbers text-center">
|
||||||
|
<div v-if="displayedLosses.attacker > 0" class="text-red-400 text-xs font-bold animate-bounce">
|
||||||
|
-{{ displayedLosses.attacker }}
|
||||||
|
</div>
|
||||||
|
<div v-if="displayedLosses.defender > 0" class="text-blue-400 text-xs font-bold animate-bounce">
|
||||||
|
-{{ displayedLosses.defender }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防守方舰队 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center justify-end gap-2 mb-2">
|
||||||
|
<span class="text-sm font-medium text-blue-400">{{ t('simulatorView.defender') }}</span>
|
||||||
|
<ShieldIcon class="h-4 w-4 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div class="fleet-display defender" :class="{ defending: attackAnimationPhase === 'attack' }">
|
||||||
|
<div class="grid grid-cols-5 gap-1 justify-end">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in currentDefenderFleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="ship-unit flex flex-col items-center p-1.5 rounded bg-blue-950/50 border border-blue-900/50"
|
||||||
|
:class="{ 'exploding': isShipExploding('defender', shipType as string) }"
|
||||||
|
>
|
||||||
|
<component :is="isDefenseType(String(shipType)) ? Shield : Rocket" class="h-4 w-4 text-blue-400" />
|
||||||
|
<span class="text-[10px] text-blue-300">{{ formatNumber(count, 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-xs text-blue-400/80 text-right">
|
||||||
|
{{ t('messagesView.power') }}: {{ formatNumber(currentDefenderPower) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 战斗日志 -->
|
||||||
|
<div class="battle-log mt-4 p-3 bg-black/30 rounded border border-white/10 max-h-32 overflow-y-auto">
|
||||||
|
<div v-for="(log, index) in battleLogs" :key="index" class="text-xs mb-1" :class="log.type">
|
||||||
|
<span class="text-muted-foreground">[{{ log.round }}]</span>
|
||||||
|
{{ log.message }}
|
||||||
|
</div>
|
||||||
|
<div v-if="battleLogs.length === 0" class="text-xs text-muted-foreground text-center py-2">
|
||||||
|
{{ t('messagesView.battleLogEmpty') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 战斗结果预览 (仅在完成时显示) -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="showResult" class="mt-4 p-4 rounded-lg border text-center" :class="resultStyle">
|
||||||
|
<p class="text-lg font-bold">{{ resultText }}</p>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">
|
||||||
|
{{ t('simulatorView.afterRounds').replace('{rounds}', String(totalRounds)) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
|
import { formatNumber } from '@/utils/format'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
|
import { Play, Pause, SkipBack, SkipForward, RotateCcw, Sword, Shield as ShieldIcon, Swords, Rocket, Shield } from 'lucide-vue-next'
|
||||||
|
import type { BattleResult, ShipType, DefenseType, Fleet } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
report: BattleResult
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'complete'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { SHIPS, DEFENSES } = useGameConfig()
|
||||||
|
|
||||||
|
// 播放状态
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
const currentRoundIndex = ref(0)
|
||||||
|
const speedMultiplier = ref('1')
|
||||||
|
const attackAnimationPhase = ref<'idle' | 'attack' | 'damage'>('idle')
|
||||||
|
const showDamageNumbers = ref(false)
|
||||||
|
const showResult = ref(false)
|
||||||
|
|
||||||
|
// 爆炸动画追踪
|
||||||
|
const explodingShips = ref<{ side: 'attacker' | 'defender'; type: string }[]>([])
|
||||||
|
|
||||||
|
// 当前显示的损失数字(用于动画显示)
|
||||||
|
const displayedLosses = ref({ attacker: 0, defender: 0 })
|
||||||
|
|
||||||
|
// 战斗日志
|
||||||
|
interface BattleLog {
|
||||||
|
round: number
|
||||||
|
message: string
|
||||||
|
type: 'attacker-loss' | 'defender-loss' | 'info'
|
||||||
|
}
|
||||||
|
const battleLogs = ref<BattleLog[]>([])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const totalRounds = computed(() => props.report.roundDetails?.length || props.report.rounds || 1)
|
||||||
|
|
||||||
|
const canPlay = computed(() => currentRoundIndex.value < totalRounds.value)
|
||||||
|
const canGoPrevious = computed(() => currentRoundIndex.value > 0)
|
||||||
|
const canGoNext = computed(() => currentRoundIndex.value < totalRounds.value)
|
||||||
|
const canRestart = computed(() => currentRoundIndex.value > 0 || battleLogs.value.length > 0)
|
||||||
|
|
||||||
|
// 当前回合的舰队状态(通过累计损失计算)
|
||||||
|
const currentAttackerFleet = computed(() => {
|
||||||
|
const fleet: Partial<Fleet> = { ...props.report.attackerFleet }
|
||||||
|
if (props.report.roundDetails) {
|
||||||
|
for (let i = 0; i < currentRoundIndex.value; i++) {
|
||||||
|
const roundLosses = props.report.roundDetails[i]?.attackerLosses || {}
|
||||||
|
for (const [shipType, count] of Object.entries(roundLosses)) {
|
||||||
|
if (fleet[shipType as keyof Fleet] !== undefined) {
|
||||||
|
fleet[shipType as keyof Fleet] = Math.max(0, (fleet[shipType as keyof Fleet] || 0) - count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 过滤掉数量为0的
|
||||||
|
return Object.fromEntries(Object.entries(fleet).filter(([, count]) => count > 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDefenderFleet = computed(() => {
|
||||||
|
const fleet: Partial<Fleet> = { ...props.report.defenderFleet }
|
||||||
|
const defense: Partial<Record<DefenseType, number>> = { ...props.report.defenderDefense }
|
||||||
|
|
||||||
|
if (props.report.roundDetails) {
|
||||||
|
for (let i = 0; i < currentRoundIndex.value; i++) {
|
||||||
|
const roundLosses = props.report.roundDetails[i]?.defenderLosses || { fleet: {}, defense: {} }
|
||||||
|
for (const [shipType, count] of Object.entries(roundLosses.fleet || {})) {
|
||||||
|
if (fleet[shipType as keyof Fleet] !== undefined) {
|
||||||
|
fleet[shipType as keyof Fleet] = Math.max(0, (fleet[shipType as keyof Fleet] || 0) - count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [defType, count] of Object.entries(roundLosses.defense || {})) {
|
||||||
|
if (defense[defType as DefenseType] !== undefined) {
|
||||||
|
defense[defType as DefenseType] = Math.max(0, (defense[defType as DefenseType] || 0) - count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并舰队和防御
|
||||||
|
const combined = {
|
||||||
|
...Object.fromEntries(Object.entries(fleet).filter(([, count]) => count > 0)),
|
||||||
|
...Object.fromEntries(Object.entries(defense).filter(([, count]) => count > 0))
|
||||||
|
}
|
||||||
|
return combined
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAttackerPower = computed(() => {
|
||||||
|
if (props.report.roundDetails && currentRoundIndex.value > 0) {
|
||||||
|
return props.report.roundDetails[currentRoundIndex.value - 1]?.attackerRemainingPower || 0
|
||||||
|
}
|
||||||
|
// 初始战斗力
|
||||||
|
return calculateFleetPower(props.report.attackerFleet)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDefenderPower = computed(() => {
|
||||||
|
if (props.report.roundDetails && currentRoundIndex.value > 0) {
|
||||||
|
return props.report.roundDetails[currentRoundIndex.value - 1]?.defenderRemainingPower || 0
|
||||||
|
}
|
||||||
|
// 初始战斗力
|
||||||
|
return calculateFleetPower(props.report.defenderFleet) + calculateDefensePower(props.report.defenderDefense)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultStyle = computed(() => {
|
||||||
|
if (props.report.winner === 'draw') {
|
||||||
|
return 'bg-gray-50 dark:bg-gray-950/30 border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
}
|
||||||
|
if (props.report.winner === 'attacker') {
|
||||||
|
return 'bg-red-50 dark:bg-red-950/30 border-red-300 dark:border-red-800 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return 'bg-blue-50 dark:bg-blue-950/30 border-blue-300 dark:border-blue-800 text-blue-700 dark:text-blue-300'
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultText = computed(() => {
|
||||||
|
if (props.report.winner === 'draw') return t('messagesView.draw')
|
||||||
|
if (props.report.winner === 'attacker') return t('messagesView.attackerWins')
|
||||||
|
return t('messagesView.defenderWins')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
const calculateFleetPower = (fleet: Partial<Fleet>): number => {
|
||||||
|
let power = 0
|
||||||
|
for (const [shipType, count] of Object.entries(fleet)) {
|
||||||
|
const config = SHIPS.value[shipType as ShipType]
|
||||||
|
if (config) {
|
||||||
|
power += (config.attack + config.shield + config.armor) * count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return power
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateDefensePower = (defense: Partial<Record<DefenseType, number>>): number => {
|
||||||
|
let power = 0
|
||||||
|
for (const [defType, count] of Object.entries(defense)) {
|
||||||
|
const config = DEFENSES.value[defType as DefenseType]
|
||||||
|
if (config) {
|
||||||
|
power += (config.attack + config.shield + config.armor) * count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return power
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDefenseType = (type: string): boolean => {
|
||||||
|
return type in DEFENSES.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShipExploding = (side: 'attacker' | 'defender', type: string): boolean => {
|
||||||
|
return explodingShips.value.some(s => s.side === side && s.type === type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStarStyle = (index: number): Record<string, string> => {
|
||||||
|
const seed = index * 1234
|
||||||
|
const x = seed % 100
|
||||||
|
const y = (seed * 7) % 100
|
||||||
|
const size = 1 + (seed % 2)
|
||||||
|
const opacity = 0.3 + (seed % 5) / 10
|
||||||
|
const delay = seed % 3000
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${x}%`,
|
||||||
|
top: `${y}%`,
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
opacity: String(opacity),
|
||||||
|
animation: `twinkle 2s ease-in-out ${delay}ms infinite`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放控制
|
||||||
|
let playTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let isPlayingRound = false // 防止重复执行
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
pause()
|
||||||
|
} else {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
if (currentRoundIndex.value >= totalRounds.value) {
|
||||||
|
restart()
|
||||||
|
}
|
||||||
|
isPlaying.value = true
|
||||||
|
scheduleNextRound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
isPlaying.value = false
|
||||||
|
if (playTimeoutId) {
|
||||||
|
clearTimeout(playTimeoutId)
|
||||||
|
playTimeoutId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleNextRound = () => {
|
||||||
|
if (!isPlaying.value) return
|
||||||
|
if (playTimeoutId) clearTimeout(playTimeoutId)
|
||||||
|
|
||||||
|
// 使用 setTimeout 而非 setInterval,确保每回合顺序执行
|
||||||
|
playTimeoutId = setTimeout(async () => {
|
||||||
|
if (!isPlaying.value) return
|
||||||
|
if (currentRoundIndex.value < totalRounds.value) {
|
||||||
|
await playRound()
|
||||||
|
scheduleNextRound() // 回合完成后再调度下一回合
|
||||||
|
} else {
|
||||||
|
pause()
|
||||||
|
showResult.value = true
|
||||||
|
emit('complete')
|
||||||
|
}
|
||||||
|
}, 100) // 短暂延迟启动
|
||||||
|
}
|
||||||
|
|
||||||
|
const playRound = async () => {
|
||||||
|
if (isPlayingRound) return // 防止重复执行
|
||||||
|
if (currentRoundIndex.value >= totalRounds.value) return
|
||||||
|
|
||||||
|
isPlayingRound = true
|
||||||
|
const speed = parseFloat(speedMultiplier.value)
|
||||||
|
const roundIndex = currentRoundIndex.value
|
||||||
|
const roundData = props.report.roundDetails?.[roundIndex]
|
||||||
|
|
||||||
|
// 攻击动画阶段
|
||||||
|
attackAnimationPhase.value = 'attack'
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
battleLogs.value.push({
|
||||||
|
round: roundIndex + 1,
|
||||||
|
message: t('messagesView.roundStarted').replace('{round}', String(roundIndex + 1)),
|
||||||
|
type: 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待攻击动画
|
||||||
|
await sleep(400 / speed)
|
||||||
|
|
||||||
|
// 伤害阶段
|
||||||
|
attackAnimationPhase.value = 'damage'
|
||||||
|
|
||||||
|
// 计算当前回合的损失数字
|
||||||
|
if (roundData) {
|
||||||
|
const attackerLoss = Object.values(roundData.attackerLosses).reduce((sum, count) => sum + count, 0)
|
||||||
|
const defenderLoss =
|
||||||
|
Object.values(roundData.defenderLosses.fleet || {}).reduce((sum, count) => sum + count, 0) +
|
||||||
|
Object.values(roundData.defenderLosses.defense || {}).reduce((sum, count) => sum + count, 0)
|
||||||
|
displayedLosses.value = { attacker: attackerLoss, defender: defenderLoss }
|
||||||
|
} else {
|
||||||
|
displayedLosses.value = { attacker: 0, defender: 0 }
|
||||||
|
}
|
||||||
|
showDamageNumbers.value = true
|
||||||
|
|
||||||
|
if (roundData) {
|
||||||
|
// 记录攻击方损失
|
||||||
|
for (const [shipType, count] of Object.entries(roundData.attackerLosses)) {
|
||||||
|
if (count > 0) {
|
||||||
|
explodingShips.value.push({ side: 'attacker', type: shipType })
|
||||||
|
battleLogs.value.push({
|
||||||
|
round: roundIndex + 1,
|
||||||
|
message: t('messagesView.shipDestroyed')
|
||||||
|
.replace('{count}', String(count))
|
||||||
|
.replace('{ship}', SHIPS.value[shipType as ShipType]?.name || shipType),
|
||||||
|
type: 'attacker-loss'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录防守方损失
|
||||||
|
for (const [shipType, count] of Object.entries(roundData.defenderLosses.fleet || {})) {
|
||||||
|
if (count > 0) {
|
||||||
|
explodingShips.value.push({ side: 'defender', type: shipType })
|
||||||
|
battleLogs.value.push({
|
||||||
|
round: roundIndex + 1,
|
||||||
|
message: t('messagesView.shipDestroyed')
|
||||||
|
.replace('{count}', String(count))
|
||||||
|
.replace('{ship}', SHIPS.value[shipType as ShipType]?.name || shipType),
|
||||||
|
type: 'defender-loss'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [defType, count] of Object.entries(roundData.defenderLosses.defense || {})) {
|
||||||
|
if (count > 0) {
|
||||||
|
explodingShips.value.push({ side: 'defender', type: defType })
|
||||||
|
battleLogs.value.push({
|
||||||
|
round: roundIndex + 1,
|
||||||
|
message: t('messagesView.defenseDestroyed')
|
||||||
|
.replace('{count}', String(count))
|
||||||
|
.replace('{defense}', DEFENSES.value[defType as DefenseType]?.name || defType),
|
||||||
|
type: 'defender-loss'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待伤害显示
|
||||||
|
await sleep(600 / speed)
|
||||||
|
|
||||||
|
// 清理状态
|
||||||
|
attackAnimationPhase.value = 'idle'
|
||||||
|
showDamageNumbers.value = false
|
||||||
|
explodingShips.value = []
|
||||||
|
|
||||||
|
currentRoundIndex.value++
|
||||||
|
isPlayingRound = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRound = () => {
|
||||||
|
if (currentRoundIndex.value < totalRounds.value) {
|
||||||
|
pause()
|
||||||
|
playRound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousRound = () => {
|
||||||
|
if (currentRoundIndex.value > 0) {
|
||||||
|
pause()
|
||||||
|
currentRoundIndex.value--
|
||||||
|
// 移除该回合的日志
|
||||||
|
battleLogs.value = battleLogs.value.filter(log => log.round <= currentRoundIndex.value)
|
||||||
|
showResult.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restart = () => {
|
||||||
|
pause()
|
||||||
|
currentRoundIndex.value = 0
|
||||||
|
battleLogs.value = []
|
||||||
|
showResult.value = false
|
||||||
|
explodingShips.value = []
|
||||||
|
attackAnimationPhase.value = 'idle'
|
||||||
|
showDamageNumbers.value = false
|
||||||
|
displayedLosses.value = { attacker: 0, defender: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number): Promise<void> => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听速度变化
|
||||||
|
watch(speedMultiplier, () => {
|
||||||
|
if (isPlaying.value) {
|
||||||
|
scheduleNextRound()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (playTimeoutId) {
|
||||||
|
clearTimeout(playTimeoutId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露给父组件
|
||||||
|
defineExpose({
|
||||||
|
currentRoundIndex,
|
||||||
|
totalRounds
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.star {
|
||||||
|
will-change: opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fleet-display {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fleet-display.attacking {
|
||||||
|
animation: shake 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fleet-display.defending {
|
||||||
|
animation: shake 0.3s ease-in-out 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate3d(-3px, 0, 0);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate3d(3px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-unit {
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ship-unit.exploding {
|
||||||
|
animation: explode 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes explode {
|
||||||
|
0% {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale3d(1.3, 1.3, 1);
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale3d(0.8, 0.8, 1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-badge {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-animation {
|
||||||
|
animation: pulse 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale3d(1.2, 1.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.damage-popup-enter-active,
|
||||||
|
.damage-popup-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.damage-popup-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, 10px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.damage-popup-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, -10px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battle-log .attacker-loss {
|
||||||
|
color: rgb(248, 113, 113);
|
||||||
|
}
|
||||||
|
|
||||||
|
.battle-log .defender-loss {
|
||||||
|
color: rgb(96, 165, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
.battle-log .info {
|
||||||
|
color: rgb(156, 163, 175);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
526
src/components/dialogs/BattleReportDialog.vue
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<ScrollableDialogContent container-class="sm:max-w-4xl max-h-[90vh]">
|
||||||
|
<template #header>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Sword class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.battleReport') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription v-if="report">
|
||||||
|
{{ formatDate(report.timestamp) }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="report" class="space-y-4">
|
||||||
|
<!-- 战斗动画切换 -->
|
||||||
|
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="flex items-center justify-between gap-2">
|
||||||
|
<!-- 左侧: 回合进度 (仅在动画模式下显示) -->
|
||||||
|
<div v-if="showAnimation && animationPlayerRef" class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||||
|
<span class="font-medium text-foreground">{{ animationPlayerRef.currentRoundIndex }}</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span>{{ animationPlayerRef.totalRounds }}</span>
|
||||||
|
<span class="text-xs">{{ t('messagesView.roundsPlayed') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else />
|
||||||
|
<!-- 右侧: 切换按钮 -->
|
||||||
|
<Button variant="outline" size="sm" @click="showAnimation = !showAnimation" class="gap-2">
|
||||||
|
<component :is="showAnimation ? FileText : Clapperboard" class="h-4 w-4" />
|
||||||
|
{{ showAnimation ? t('messagesView.showDetails') : t('messagesView.playAnimation') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 战斗动画播放器 -->
|
||||||
|
<BattleAnimationPlayer
|
||||||
|
v-if="showAnimation && report.roundDetails && report.roundDetails.length > 0"
|
||||||
|
ref="animationPlayerRef"
|
||||||
|
:report="report"
|
||||||
|
@complete="onAnimationComplete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 详细信息(动画播放时隐藏) -->
|
||||||
|
<template v-if="!showAnimation">
|
||||||
|
<!-- 战斗双方信息 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- 攻击方星球 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<Sword class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold">{{ t('simulatorView.attacker') }}</p>
|
||||||
|
<p v-if="attackerPlanet" class="text-sm text-muted-foreground truncate">
|
||||||
|
{{ attackerPlanet.name }} [{{ attackerPlanet.position.galaxy }}:{{ attackerPlanet.position.system }}:{{
|
||||||
|
attackerPlanet.position.position
|
||||||
|
}}]
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-sm text-muted-foreground">{{ report.attackerPlanetId }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防守方星球 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<ShieldIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold">{{ t('simulatorView.defender') }}</p>
|
||||||
|
<p v-if="defenderPlanet" class="text-sm text-muted-foreground truncate">
|
||||||
|
{{ defenderPlanet.name }} [{{ defenderPlanet.position.galaxy }}:{{ defenderPlanet.position.system }}:{{
|
||||||
|
defenderPlanet.position.position
|
||||||
|
}}]
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-sm text-muted-foreground">{{ report.defenderPlanetId }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 胜利者 -->
|
||||||
|
<div class="text-center p-5 rounded-lg border" :class="getPlayerResultStyle()">
|
||||||
|
<p class="text-xl font-bold">
|
||||||
|
{{
|
||||||
|
report.winner === 'draw' ? t('messagesView.draw') : isPlayerVictory ? t('messagesView.victory') : t('messagesView.defeat')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p v-if="report.rounds" class="text-sm text-muted-foreground mt-1">
|
||||||
|
{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 损失对比 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.losses') }}</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- 攻击方损失 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-sm font-medium">{{ t('simulatorView.attacker') }}</span>
|
||||||
|
<Badge variant="secondary" class="text-xs">{{ getTotalLossCount(report.attackerLosses) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<div v-for="(count, shipType) in report.attackerLosses" :key="shipType" class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-muted-foreground truncate">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-medium text-destructive">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="Object.keys(report.attackerLosses).length === 0" class="text-xs text-muted-foreground text-center py-2">
|
||||||
|
{{ t('messagesView.noLosses') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防守方损失 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-sm font-medium">{{ t('simulatorView.defender') }}</span>
|
||||||
|
<Badge variant="secondary" class="text-xs">{{ getTotalDefenderLossCount(report.defenderLosses) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in report.defenderLosses.fleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex items-center justify-between text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground truncate">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-medium text-destructive">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(count, defenseType) in report.defenderLosses.defense"
|
||||||
|
:key="defenseType"
|
||||||
|
class="flex items-center justify-between text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground truncate">{{ DEFENSES[defenseType].name }}</span>
|
||||||
|
<span class="font-medium text-destructive">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="Object.keys(report.defenderLosses.fleet).length === 0 && Object.keys(report.defenderLosses.defense).length === 0"
|
||||||
|
class="text-xs text-muted-foreground text-center py-2"
|
||||||
|
>
|
||||||
|
{{ t('messagesView.noLosses') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 剩余单位 -->
|
||||||
|
<div v-if="hasAnyRemaining" class="space-y-3">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.remainingUnits') }}</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- 攻击方剩余 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-sm font-medium">{{ t('simulatorView.attacker') }}</span>
|
||||||
|
<Badge v-if="report.attackerRemaining" variant="outline" class="text-xs">
|
||||||
|
{{ getTotalLossCount(report.attackerRemaining) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<template v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in report.attackerRemaining"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex items-center justify-between p-1.5 bg-white/50 dark:bg-black/20 rounded text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground truncate">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-bold">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="text-xs text-muted-foreground text-center py-2">{{ t('messagesView.allDestroyed') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防守方剩余 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-sm font-medium">{{ t('simulatorView.defender') }}</span>
|
||||||
|
<Badge v-if="report.defenderRemaining" variant="outline" class="text-xs">
|
||||||
|
{{ getTotalDefenderRemainingCount(report.defenderRemaining) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
report.defenderRemaining &&
|
||||||
|
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
|
||||||
|
Object.keys(report.defenderRemaining.defense || {}).length > 0)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in report.defenderRemaining.fleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex items-center justify-between p-1.5 bg-white/50 dark:bg-black/20 rounded text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground truncate">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-bold">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(count, defenseType) in report.defenderRemaining.defense"
|
||||||
|
:key="defenseType"
|
||||||
|
class="flex items-center justify-between p-1.5 bg-white/50 dark:bg-black/20 rounded text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground truncate">{{ DEFENSES[defenseType].name }}</span>
|
||||||
|
<span class="font-bold">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="text-xs text-muted-foreground text-center py-2">{{ t('messagesView.allDestroyed') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 掠夺资源 -->
|
||||||
|
<div
|
||||||
|
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Package class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.plunder') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div v-if="report.plunder.metal > 0" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="metal" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.metal') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">+{{ formatNumber(report.plunder.metal) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="report.plunder.crystal > 0" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="crystal" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.crystal') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">+{{ formatNumber(report.plunder.crystal) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="report.plunder.deuterium > 0" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="deuterium" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.deuterium') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">+{{ formatNumber(report.plunder.deuterium) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 残骸场 -->
|
||||||
|
<div v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Recycle class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.debrisField') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div v-if="report.debrisField.metal > 0" class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<ResourceIcon type="metal" size="sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('resources.metal') }}</p>
|
||||||
|
<p class="font-bold">{{ formatNumber(report.debrisField.metal) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="report.debrisField.crystal > 0" class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<ResourceIcon type="crystal" size="sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ t('resources.crystal') }}</p>
|
||||||
|
<p class="font-bold">{{ formatNumber(report.debrisField.crystal) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 月球生成概率 -->
|
||||||
|
<div v-if="report.moonChance && report.moonChance > 0" class="mt-3 pt-3 border-t">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<Moon class="h-4 w-4" />
|
||||||
|
<span class="text-sm">{{ t('messagesView.moonChance') }}:</span>
|
||||||
|
<span class="font-bold">{{ (report.moonChance * 100).toFixed(1) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回合详情 -->
|
||||||
|
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-3">
|
||||||
|
<Button @click="showRoundDetails = !showRoundDetails" variant="outline" size="sm" class="w-full gap-2">
|
||||||
|
<ListOrdered class="h-4 w-4" />
|
||||||
|
{{ showRoundDetails ? t('messagesView.hideRoundDetails') : t('messagesView.showRoundDetails') }}
|
||||||
|
<ChevronDown class="h-4 w-4 transition-transform" :class="{ 'rotate-180': showRoundDetails }" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div v-if="showRoundDetails" class="relative pl-6 space-y-4">
|
||||||
|
<!-- 时间线 -->
|
||||||
|
<div class="absolute left-2 top-0 bottom-0 w-0.5 bg-border" />
|
||||||
|
|
||||||
|
<div v-for="detail in report.roundDetails" :key="detail.round" class="relative">
|
||||||
|
<!-- 时间线节点 -->
|
||||||
|
<div class="absolute -left-6 top-3 w-4 h-4 rounded-full bg-primary border-2 border-background shadow-md" />
|
||||||
|
|
||||||
|
<!-- 回合内容卡片 -->
|
||||||
|
<div class="border rounded-lg p-4 bg-card hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge variant="outline">{{ t('messagesView.round').replace('{round}', String(detail.round)) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<TooltipProvider :delay-duration="300">
|
||||||
|
<div class="flex gap-4 text-xs">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="flex items-center gap-1.5">
|
||||||
|
<Sword class="h-3.5 w-3.5" />
|
||||||
|
<span class="font-medium">{{ formatNumber(detail.attackerRemainingPower) }}</span>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ t('messagesView.attackerRemainingPower') }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="flex items-center gap-1.5">
|
||||||
|
<ShieldIcon class="h-3.5 w-3.5" />
|
||||||
|
<span class="font-medium">{{ formatNumber(detail.defenderRemainingPower) }}</span>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ t('messagesView.defenderRemainingPower') }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<!-- 攻击方本回合损失 -->
|
||||||
|
<div class="bg-muted/50 rounded-lg p-3 border">
|
||||||
|
<p class="text-xs font-medium mb-2">{{ t('messagesView.attackerLosses') }}</p>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in detail.attackerLosses"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex justify-between text-xs p-1 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-bold text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="Object.keys(detail.attackerLosses).length === 0" class="text-xs text-muted-foreground text-center py-1">
|
||||||
|
{{ t('messagesView.noLosses') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防守方本回合损失 -->
|
||||||
|
<div class="bg-muted/50 rounded-lg p-3 border">
|
||||||
|
<p class="text-xs font-medium mb-2">{{ t('messagesView.defenderLosses') }}</p>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in detail.defenderLosses.fleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex justify-between text-xs p-1 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-bold text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(count, defenseType) in detail.defenderLosses.defense"
|
||||||
|
:key="defenseType"
|
||||||
|
class="flex justify-between text-xs p-1 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}</span>
|
||||||
|
<span class="font-bold text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="
|
||||||
|
Object.keys(detail.defenderLosses.fleet).length === 0 && Object.keys(detail.defenderLosses.defense).length === 0
|
||||||
|
"
|
||||||
|
class="text-xs text-muted-foreground text-center py-1"
|
||||||
|
>
|
||||||
|
{{ t('messagesView.noLosses') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ScrollableDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useUniverseStore } from '@/stores/universeStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
|
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||||
|
import { formatNumber, formatDate } from '@/utils/format'
|
||||||
|
import { Sword, Shield as ShieldIcon, Package, Recycle, Moon, ListOrdered, ChevronDown, Clapperboard, FileText } from 'lucide-vue-next'
|
||||||
|
import BattleAnimationPlayer from './BattleAnimationPlayer.vue'
|
||||||
|
import type { BattleResult } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
report: BattleResult | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const universeStore = useUniverseStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { SHIPS, DEFENSES } = useGameConfig()
|
||||||
|
|
||||||
|
const isOpen = ref(props.open)
|
||||||
|
const showRoundDetails = ref(false)
|
||||||
|
const showAnimation = ref(false)
|
||||||
|
const animationPlayerRef = ref<InstanceType<typeof BattleAnimationPlayer> | null>(null)
|
||||||
|
|
||||||
|
const onAnimationComplete = () => {
|
||||||
|
// 动画完成后可以选择自动切换到详情视图
|
||||||
|
// showAnimation.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取攻击方星球信息
|
||||||
|
const attackerPlanet = computed(() => {
|
||||||
|
if (!props.report) return null
|
||||||
|
// 先从玩家星球中查找
|
||||||
|
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
||||||
|
if (playerPlanet) return playerPlanet
|
||||||
|
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||||
|
return Object.values(universeStore.planets).find(p => p.id === props.report!.attackerPlanetId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取防守方星球信息
|
||||||
|
const defenderPlanet = computed(() => {
|
||||||
|
if (!props.report) return null
|
||||||
|
// 先从玩家星球中查找
|
||||||
|
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
|
||||||
|
if (playerPlanet) return playerPlanet
|
||||||
|
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||||
|
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断玩家是攻击方还是防守方
|
||||||
|
const isPlayerAttacker = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
return gameStore.player.planets.some(p => p.id === props.report!.attackerPlanetId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断玩家是否胜利
|
||||||
|
const isPlayerVictory = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
if (props.report.winner === 'draw') return false
|
||||||
|
// 玩家是攻击方且攻击方胜利,或者玩家是防守方且防守方胜利
|
||||||
|
return (isPlayerAttacker.value && props.report.winner === 'attacker') || (!isPlayerAttacker.value && props.report.winner === 'defender')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否有任何剩余单位需要显示
|
||||||
|
const hasAnyRemaining = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
const hasAttackerRemaining = props.report.attackerRemaining && Object.keys(props.report.attackerRemaining).length > 0
|
||||||
|
const hasDefenderRemaining =
|
||||||
|
props.report.defenderRemaining &&
|
||||||
|
(Object.keys(props.report.defenderRemaining.fleet || {}).length > 0 ||
|
||||||
|
Object.keys(props.report.defenderRemaining.defense || {}).length > 0)
|
||||||
|
return hasAttackerRemaining || hasDefenderRemaining
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
newValue => {
|
||||||
|
isOpen.value = newValue
|
||||||
|
if (newValue) {
|
||||||
|
showRoundDetails.value = false
|
||||||
|
showAnimation.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(isOpen, newValue => {
|
||||||
|
emit('update:open', newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取玩家战斗结果样式
|
||||||
|
const getPlayerResultStyle = () => {
|
||||||
|
if (!props.report) return 'bg-gray-50 dark:bg-gray-950/30 border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
if (props.report.winner === 'draw')
|
||||||
|
return 'bg-gray-50 dark:bg-gray-950/30 border-gray-200 dark:border-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
if (isPlayerVictory.value)
|
||||||
|
return 'bg-green-50 dark:bg-green-950/30 border-green-300 dark:border-green-800 text-green-700 dark:text-green-300'
|
||||||
|
return 'bg-red-50 dark:bg-red-950/30 border-red-300 dark:border-red-800 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取攻击方损失总数
|
||||||
|
const getTotalLossCount = (losses: Record<string, number>): number => {
|
||||||
|
return Object.values(losses).reduce((sum, count) => sum + count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取防守方损失总数
|
||||||
|
const getTotalDefenderLossCount = (losses: { fleet: Record<string, number>; defense: Record<string, number> }): number => {
|
||||||
|
const fleetLoss = Object.values(losses.fleet || {}).reduce((sum, count) => sum + count, 0)
|
||||||
|
const defenseLoss = Object.values(losses.defense || {}).reduce((sum, count) => sum + count, 0)
|
||||||
|
return fleetLoss + defenseLoss
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取防守方剩余总数
|
||||||
|
const getTotalDefenderRemainingCount = (remaining: { fleet?: Record<string, number>; defense?: Record<string, number> }): number => {
|
||||||
|
const fleetCount = Object.values(remaining.fleet || {}).reduce((sum, count) => sum + count, 0)
|
||||||
|
const defenseCount = Object.values(remaining.defense || {}).reduce((sum, count) => sum + count, 0)
|
||||||
|
return fleetCount + defenseCount
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
import { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import ItemDetailView from './ItemDetailView.vue'
|
import ItemDetailView from '@/components/common/ItemDetailView.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const dialogStore = useDetailDialogStore()
|
const dialogStore = useDetailDialogStore()
|
||||||
346
src/components/dialogs/MissionReportDialog.vue
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<component :is="getMissionIcon(report?.missionType)" class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.missionReportDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('messagesView.missionDetails') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="report" class="space-y-4">
|
||||||
|
<!-- 任务状态 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{{ getMissionTypeName(report.missionType) }}</h3>
|
||||||
|
<Badge :variant="report.success ? 'default' : 'destructive'">
|
||||||
|
{{ report.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground mb-2">
|
||||||
|
{{ formatDate(report.timestamp) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">{{ report.message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 起点和终点 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.origin') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium">{{ report.originPlanetName }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.destination') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium" v-if="report.targetPlanetName">{{ report.targetPlanetName }}</p>
|
||||||
|
<p class="text-sm text-muted-foreground" v-else>
|
||||||
|
[{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{ report.targetPosition.position }}]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务详情 -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- 运输任务详情 -->
|
||||||
|
<div v-if="report.details?.transportedResources" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Package class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.transportedResources') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div v-for="res in basicResourceFields" :key="res.key" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon :type="res.key" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t(`resources.${res.key}`) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">
|
||||||
|
{{ report.details.transportedResources[res.key].toLocaleString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回收任务详情 -->
|
||||||
|
<div v-if="report.details?.recycledResources" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Recycle class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.recycledResources') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div v-for="res in debrisResourceFields" :key="res.key" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon :type="res.key" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t(`resources.${res.key}`) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">+{{ report.details.recycledResources[res.key].toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 剩余残骸 -->
|
||||||
|
<div v-if="report.details.remainingDebris" class="mt-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<AlertTriangle class="h-4 w-4" />
|
||||||
|
<span class="text-sm font-medium text-muted-foreground">{{ t('messagesView.remainingDebris') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div v-for="res in debrisResourceFields" :key="res.key" class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon :type="res.key" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t(`resources.${res.key}`) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">{{ report.details.remainingDebris[res.key].toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 殖民任务详情 -->
|
||||||
|
<div v-if="report.details?.newPlanetName">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.newPlanet') }}:</p>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<Globe class="h-4 w-4" />
|
||||||
|
<span class="font-medium">{{ report.details.newPlanetName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导弹攻击详情 -->
|
||||||
|
<div v-if="report.details?.missileCount !== undefined" class="space-y-4">
|
||||||
|
<!-- 导弹统计卡片 -->
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<!-- 发射数量 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<Rocket class="h-4 w-4" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('galaxyView.missileCount') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold">{{ report.details.missileCount }}</p>
|
||||||
|
</div>
|
||||||
|
<!-- 命中数量 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<Target class="h-4 w-4" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('missionReports.hits') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold">{{ report.details.missileHits }}</p>
|
||||||
|
</div>
|
||||||
|
<!-- 被拦截数量 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ShieldAlert class="h-4 w-4" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('galaxyView.intercepted') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold">{{ report.details.missileIntercepted }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防御损失 -->
|
||||||
|
<div v-if="Object.keys(report.details.defenseLosses || {}).length > 0">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Flame class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('galaxyView.defenseLosses') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(count, defenseType) in report.details.defenseLosses"
|
||||||
|
:key="defenseType"
|
||||||
|
class="flex items-center justify-between p-2 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-muted-foreground">{{ t('defenses.' + defenseType) }}</span>
|
||||||
|
<span class="font-bold text-destructive">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无损失提示 -->
|
||||||
|
<div
|
||||||
|
v-else-if="report.details.missileHits === 0"
|
||||||
|
class="p-3 bg-muted/50 rounded-lg border flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ShieldCheck class="h-5 w-5" />
|
||||||
|
<span class="text-sm">{{ t('messagesView.noLosses') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 探险区域 -->
|
||||||
|
<div v-if="report.missionType === MissionType.Expedition && report.details?.expeditionZone" class="space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('fleetView.expeditionZone') }}:</p>
|
||||||
|
<div class="p-2 bg-muted/50 rounded flex items-center gap-2">
|
||||||
|
<MapPin class="h-4 w-4 text-primary" />
|
||||||
|
<span class="font-medium">{{ t(`fleetView.zones.${report.details.expeditionZone}.name`) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 发现资源 -->
|
||||||
|
<div v-if="report.details?.foundResources" class="space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.resources') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-muted/50 rounded">
|
||||||
|
<div v-for="res in allResourceFields" :key="res.key">
|
||||||
|
<template v-if="(report.details?.foundResources?.[res.key] ?? 0) > 0">
|
||||||
|
<span class="text-muted-foreground">{{ t(`resources.${res.key}`) }}:</span>
|
||||||
|
<span class="ml-1 font-medium">
|
||||||
|
+{{ (report.details?.foundResources?.[res.key] ?? 0).toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 发现舰船 -->
|
||||||
|
<div v-if="report.details?.foundFleet" class="space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.fleet') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-muted/50 rounded">
|
||||||
|
<div v-for="(count, shipType) in report.details.foundFleet" :key="shipType">
|
||||||
|
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||||
|
<span class="ml-1 font-medium">+{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 损失舰船 -->
|
||||||
|
<div v-if="report.details?.fleetLost" class="space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.attackerLosses') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-muted/50 rounded">
|
||||||
|
<div v-for="(count, shipType) in report.details.fleetLost" :key="shipType">
|
||||||
|
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-destructive">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="isOpen = false">{{ t('common.close') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||||
|
import { formatDate } from '@/utils/format'
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Recycle,
|
||||||
|
AlertTriangle,
|
||||||
|
Globe,
|
||||||
|
Rocket,
|
||||||
|
Target,
|
||||||
|
ShieldAlert,
|
||||||
|
Flame,
|
||||||
|
ShieldCheck,
|
||||||
|
Truck,
|
||||||
|
Eye,
|
||||||
|
Sword,
|
||||||
|
Compass,
|
||||||
|
Skull,
|
||||||
|
MapPin
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { MissionType } from '@/types/game'
|
||||||
|
import type { MissionReport } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
report: MissionReport | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isOpen = ref(props.open)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
newValue => {
|
||||||
|
isOpen.value = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(isOpen, newValue => {
|
||||||
|
emit('update:open', newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 资源字段配置
|
||||||
|
type BasicResourceKey = 'metal' | 'crystal' | 'deuterium'
|
||||||
|
const basicResourceFields: { key: BasicResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }]
|
||||||
|
|
||||||
|
type DebrisResourceKey = 'metal' | 'crystal'
|
||||||
|
const debrisResourceFields: { key: DebrisResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }]
|
||||||
|
|
||||||
|
type AllResourceKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||||
|
const allResourceFields: { key: AllResourceKey }[] = [
|
||||||
|
{ key: 'metal' },
|
||||||
|
{ key: 'crystal' },
|
||||||
|
{ key: 'deuterium' },
|
||||||
|
{ key: 'darkMatter' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取任务类型名称
|
||||||
|
const getMissionTypeName = (missionType?: MissionType): string => {
|
||||||
|
if (missionType === undefined) return ''
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Transport:
|
||||||
|
return t('fleetView.transport')
|
||||||
|
case MissionType.Deploy:
|
||||||
|
return t('fleetView.deploy')
|
||||||
|
case MissionType.Attack:
|
||||||
|
return t('fleetView.attackMission')
|
||||||
|
case MissionType.Spy:
|
||||||
|
return t('fleetView.spy')
|
||||||
|
case MissionType.Colonize:
|
||||||
|
return t('fleetView.colonize')
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return t('fleetView.recycle')
|
||||||
|
case MissionType.Expedition:
|
||||||
|
return t('fleetView.expedition')
|
||||||
|
case MissionType.Destroy:
|
||||||
|
return t('fleetView.destroy')
|
||||||
|
case MissionType.MissileAttack:
|
||||||
|
return t('galaxyView.missileAttack')
|
||||||
|
default:
|
||||||
|
return t('common.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务图标
|
||||||
|
const getMissionIcon = (missionType?: MissionType) => {
|
||||||
|
if (missionType === undefined) return Package
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Transport:
|
||||||
|
return Truck
|
||||||
|
case MissionType.Deploy:
|
||||||
|
return Package
|
||||||
|
case MissionType.Attack:
|
||||||
|
return Sword
|
||||||
|
case MissionType.Spy:
|
||||||
|
return Eye
|
||||||
|
case MissionType.Colonize:
|
||||||
|
return Globe
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return Recycle
|
||||||
|
case MissionType.MissileAttack:
|
||||||
|
return Rocket
|
||||||
|
case MissionType.Expedition:
|
||||||
|
return Compass
|
||||||
|
case MissionType.Destroy:
|
||||||
|
return Skull
|
||||||
|
default:
|
||||||
|
return Package
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
152
src/components/dialogs/NPCActivityDialog.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Recycle class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.npcActivityDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('messagesView.activityDescription') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="notification" class="space-y-4">
|
||||||
|
<!-- NPC信息 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{{ npcName }}</h3>
|
||||||
|
<Badge variant="secondary">{{ t('messagesView.activityType.' + notification.activityType) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatDate(notification.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活动位置 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.activityLocation') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Globe class="h-4 w-4" />
|
||||||
|
<span class="font-medium">
|
||||||
|
{{ t('messagesView.position') }}: [{{ notification.targetPosition.galaxy }}:{{
|
||||||
|
notification.targetPosition.system
|
||||||
|
}}:{{ notification.targetPosition.position }}]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="notification.targetPlanetName" class="text-sm text-muted-foreground">
|
||||||
|
{{ t('messagesView.nearPlanet') }}: {{ notification.targetPlanetName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活动描述 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.activityDescription') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="text-sm">
|
||||||
|
{{
|
||||||
|
t('messagesView.npcActivityMessage', {
|
||||||
|
npc: npcName,
|
||||||
|
activity: t('messagesView.activityType.' + notification.activityType),
|
||||||
|
position: `[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 到达时间 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.arrivalTime') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium">{{ formatDate(notification.arrivalTime) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-md border">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ t('messagesView.npcActivityTip') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="isOpen = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="viewLocationInGalaxy">{{ t('messagesView.viewInGalaxy') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { formatDate } from '@/utils/format'
|
||||||
|
import { Recycle, Globe } from 'lucide-vue-next'
|
||||||
|
import type { NPCActivityNotification } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
notification: NPCActivityNotification | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const npcStore = useNPCStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isOpen = ref(props.open)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
newValue => {
|
||||||
|
isOpen.value = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(isOpen, newValue => {
|
||||||
|
emit('update:open', newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取NPC名称
|
||||||
|
const npcName = computed(() => {
|
||||||
|
if (!props.notification) return ''
|
||||||
|
if (!npcStore.npcs?.length) return props.notification.npcName
|
||||||
|
|
||||||
|
// 通过 npcId 查找
|
||||||
|
if (props.notification.npcId) {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === props.notification!.npcId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从旧名称中提取ID并查找
|
||||||
|
const idMatch = props.notification.npcName.match(/npc_\d+/)
|
||||||
|
if (idMatch) {
|
||||||
|
const extractedId = idMatch[0]
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === extractedId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.notification.npcName
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在银河系中查看位置
|
||||||
|
const viewLocationInGalaxy = () => {
|
||||||
|
if (!props.notification?.targetPosition) return
|
||||||
|
isOpen.value = false
|
||||||
|
router.push(
|
||||||
|
`/galaxy?galaxy=${props.notification.targetPosition.galaxy}&system=${props.notification.targetPosition.system}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../../package.json'
|
||||||
|
|
||||||
// 双向绑定 open 状态
|
// 双向绑定 open 状态
|
||||||
const open = defineModel<boolean>('open', { default: false })
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
147
src/components/dialogs/SpiedNotificationDialog.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<DialogContent class="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Eye class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.spiedNotificationDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">
|
||||||
|
{{ t('messagesView.spyDetected') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="notification" class="space-y-4">
|
||||||
|
<!-- 侦查者信息 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<AlertTriangle class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<h3 class="font-semibold">{{ npcName }}</h3>
|
||||||
|
<Badge :variant="notification.detectionSuccess ? 'destructive' : 'secondary'" class="text-xs">
|
||||||
|
{{ notification.detectionSuccess ? t('messagesView.detected') : t('messagesView.undetected') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatDate(notification.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 被侦查星球 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.targetPlanet') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md border flex items-center gap-2">
|
||||||
|
<Globe class="h-4 w-4" />
|
||||||
|
<span class="font-medium">{{ notification.targetPlanetName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.detectionResult') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md border">
|
||||||
|
<p class="text-sm">
|
||||||
|
{{
|
||||||
|
t('messagesView.spiedNotificationMessage', {
|
||||||
|
npc: npcName,
|
||||||
|
planet: notification.targetPlanetName
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 建议 -->
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md border">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ t('messagesView.spiedNotificationTip') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="isOpen = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="viewNPCInGalaxy">{{ t('messagesView.viewInGalaxy') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { formatDate } from '@/utils/format'
|
||||||
|
import { Eye, AlertTriangle, Globe } from 'lucide-vue-next'
|
||||||
|
import type { SpiedNotification } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
notification: SpiedNotification | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const npcStore = useNPCStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isOpen = ref(props.open)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
newValue => {
|
||||||
|
isOpen.value = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(isOpen, newValue => {
|
||||||
|
emit('update:open', newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取NPC名称
|
||||||
|
const npcName = computed(() => {
|
||||||
|
if (!props.notification) return ''
|
||||||
|
if (!npcStore.npcs?.length) return props.notification.npcName
|
||||||
|
|
||||||
|
// 通过 npcId 查找
|
||||||
|
if (props.notification.npcId) {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === props.notification!.npcId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从旧名称中提取ID并查找
|
||||||
|
const idMatch = props.notification.npcName.match(/npc_\d+/)
|
||||||
|
if (idMatch) {
|
||||||
|
const extractedId = idMatch[0]
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === extractedId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.notification.npcName
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在银河系中查看NPC
|
||||||
|
const viewNPCInGalaxy = () => {
|
||||||
|
if (!props.notification?.npcId) return
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === props.notification!.npcId)
|
||||||
|
if (npc && npc.planets && npc.planets.length > 0) {
|
||||||
|
isOpen.value = false
|
||||||
|
const homePlanet = npc.planets[0]?.position
|
||||||
|
if (homePlanet) {
|
||||||
|
router.push(`/galaxy?galaxy=${homePlanet.galaxy}&system=${homePlanet.system}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
201
src/components/dialogs/SpyReportDialog.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="isOpen">
|
||||||
|
<ScrollableDialogContent container-class="sm:max-w-2xl max-h-[90vh]">
|
||||||
|
<template #header>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Eye class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.spyReport') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription v-if="report">
|
||||||
|
{{ formatDate(report.timestamp) }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="report" class="space-y-4">
|
||||||
|
<!-- 目标星球信息 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-background rounded-full border">
|
||||||
|
<Globe class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold">{{ report.targetPlanetName }}</p>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
[{{ report.targetPosition.galaxy }}:{{ report.targetPosition.system }}:{{ report.targetPosition.position }}]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 资源 -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Coins class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.resources') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
<!-- 金属 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="metal" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.metal') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">
|
||||||
|
{{ formatNumber(report.resources.metal) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- 晶体 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="crystal" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.crystal') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">
|
||||||
|
{{ formatNumber(report.resources.crystal) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- 氘 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="deuterium" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.deuterium') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">
|
||||||
|
{{ formatNumber(report.resources.deuterium) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- 暗物质 -->
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<ResourceIcon type="darkMatter" size="sm" />
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('resources.darkMatter') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-bold">
|
||||||
|
{{ formatNumber(report.resources.darkMatter) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 舰队(如果有) -->
|
||||||
|
<div v-if="report.fleet && Object.keys(report.fleet).length > 0" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Rocket class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.fleet') }}</h4>
|
||||||
|
<Badge variant="secondary" class="text-xs">{{ getTotalFleetCount(report.fleet) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(count, shipType) in report.fleet"
|
||||||
|
:key="shipType"
|
||||||
|
class="flex items-center justify-between p-2 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-muted-foreground truncate">{{ SHIPS[shipType].name }}</span>
|
||||||
|
<span class="font-bold">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 防御设施(如果有) -->
|
||||||
|
<div v-if="report.defense && hasDefense(report.defense)" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Shield class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.defense') }}</h4>
|
||||||
|
<Badge variant="secondary" class="text-xs">{{ getTotalDefenseCount(report.defense) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(count, defenseType) in report.defense"
|
||||||
|
:key="defenseType"
|
||||||
|
class="flex items-center justify-between p-2 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<template v-if="count && count > 0">
|
||||||
|
<span class="text-sm text-muted-foreground truncate">{{ DEFENSES[defenseType].name }}</span>
|
||||||
|
<span class="font-bold">{{ count }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 建筑(如果有) -->
|
||||||
|
<div v-if="report.buildings && Object.keys(report.buildings).length > 0" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Building class="h-4 w-4" />
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.buildings') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(level, buildingType) in report.buildings"
|
||||||
|
:key="buildingType"
|
||||||
|
class="flex items-center justify-between p-2 bg-white/50 dark:bg-black/20 rounded"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-muted-foreground truncate">{{ BUILDINGS[buildingType].name }}</span>
|
||||||
|
<Badge variant="outline" class="font-bold">Lv.{{ level }}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollableDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
|
import { Dialog, ScrollableDialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||||
|
import { formatNumber, formatDate } from '@/utils/format'
|
||||||
|
import { Eye, Globe, Coins, Rocket, Shield, Building } from 'lucide-vue-next'
|
||||||
|
import type { SpyReport } from '@/types/game'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
report: SpyReport | null
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { SHIPS, DEFENSES, BUILDINGS } = useGameConfig()
|
||||||
|
|
||||||
|
const isOpen = ref(props.open)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
newValue => {
|
||||||
|
isOpen.value = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(isOpen, newValue => {
|
||||||
|
emit('update:open', newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否有防御设施
|
||||||
|
const hasDefense = (defense: any): boolean => {
|
||||||
|
if (!defense) return false
|
||||||
|
return Object.values(defense).some((count: any) => count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取舰队总数
|
||||||
|
const getTotalFleetCount = (fleet: Record<string, number>): number => {
|
||||||
|
return Object.values(fleet).reduce((sum, count) => sum + count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取防御总数
|
||||||
|
const getTotalDefenseCount = (defense: Record<string, number>): number => {
|
||||||
|
return Object.values(defense).reduce((sum, count) => sum + (count || 0), 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="open" @update:open="$emit('update:open', $event)">
|
<Dialog :open="open" @update:open="$emit('update:open', $event)">
|
||||||
<DialogScrollContent class="max-w-2xl max-h-[80vh] flex flex-col">
|
<DialogScrollContent class="max-w-2xl max-h-[80vh] flex flex-col">
|
||||||
<DialogHeader class="flex-shrink-0">
|
<DialogHeader class="shrink-0">
|
||||||
<DialogTitle>{{ t('settings.newVersionAvailable', { version: versionInfo?.version || '' }) }}</DialogTitle>
|
<DialogTitle>{{ t('settings.newVersionAvailable', { version: versionInfo?.version || '' }) }}</DialogTitle>
|
||||||
<DialogDescription>{{ t('settings.updateAvailable') }}</DialogDescription>
|
<DialogDescription>{{ t('settings.updateAvailable') }}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown" />
|
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="flex gap-2 flex-shrink-0 mt-4">
|
<DialogFooter class="flex gap-2 shrink-0 mt-4">
|
||||||
<Button variant="outline" @click="$emit('update:open', false)">
|
<Button variant="outline" @click="$emit('update:open', false)">
|
||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- 左侧:事件图标 -->
|
<!-- 左侧:事件图标 -->
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.eventType)" />
|
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.eventType)" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 中间:主要信息 -->
|
<!-- 中间:主要信息 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm truncate">{{ report.npcName }}</span>
|
<span class="font-medium text-sm truncate">{{ getNpcName(report) }}</span>
|
||||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs flex-shrink-0">
|
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs shrink-0">
|
||||||
{{ getStatusText(report.newStatus) }}
|
{{ getStatusText(report.newStatus) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧:好感度变化和时间 -->
|
<!-- 右侧:好感度变化和时间 -->
|
||||||
<div class="flex-shrink-0 text-right">
|
<div class="shrink-0 text-right">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-bold block"
|
class="text-sm font-bold block"
|
||||||
:class="report.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
:class="report.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 未读标记 -->
|
<!-- 未读标记 -->
|
||||||
<span v-if="!report.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0" />
|
<span v-if="!report.read" class="h-2 w-2 rounded-full bg-destructive shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<div class="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
|
<div class="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<h3 class="font-semibold text-lg">{{ selectedReport.npcName }}</h3>
|
<h3 class="font-semibold text-lg">{{ getNpcName(selectedReport) }}</h3>
|
||||||
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)">
|
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)">
|
||||||
{{ getStatusText(selectedReport.newStatus) }}
|
{{ getStatusText(selectedReport.newStatus) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<p class="text-sm p-3 bg-muted/30 rounded-md">
|
<p class="text-sm p-3 bg-muted/30 rounded-md">
|
||||||
{{
|
{{
|
||||||
selectedReport.messageKey && selectedReport.messageParams
|
selectedReport.messageKey && selectedReport.messageParams
|
||||||
? t(selectedReport.messageKey, selectedReport.messageParams)
|
? t(selectedReport.messageKey, getMessageParams(selectedReport))
|
||||||
: selectedReport.message
|
: selectedReport.message
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -195,6 +195,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -209,6 +210,7 @@
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const npcStore = useNPCStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const detailDialogOpen = ref(false)
|
const detailDialogOpen = ref(false)
|
||||||
@@ -218,6 +220,42 @@
|
|||||||
return (gameStore.player.diplomaticReports || []).slice().reverse().slice(0, 20) // 最近20条
|
return (gameStore.player.diplomaticReports || []).slice().reverse().slice(0, 20) // 最近20条
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取NPC当前名称
|
||||||
|
* 优先使用当前NPC的实际名称,如果NPC不存在则尝试从旧名称中提取ID查找
|
||||||
|
*/
|
||||||
|
const getNpcName = (report: DiplomaticReport): string => {
|
||||||
|
if (!npcStore.npcs?.length) return report.npcName
|
||||||
|
|
||||||
|
// 1. 先通过 npcId 查找
|
||||||
|
if (report.npcId) {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === report.npcId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试从旧名称中提取ID并查找
|
||||||
|
// 旧格式如 "NPC-npc_182",新ID格式为 "npc_182"
|
||||||
|
const idMatch = report.npcName.match(/npc_\d+/)
|
||||||
|
if (idMatch) {
|
||||||
|
const extractedId = idMatch[0]
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === extractedId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return report.npcName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取报告的消息参数,将 npcName 替换为当前名称
|
||||||
|
*/
|
||||||
|
const getMessageParams = (report: DiplomaticReport): Record<string, string | number> => {
|
||||||
|
if (!report.messageParams) return {}
|
||||||
|
return {
|
||||||
|
...report.messageParams,
|
||||||
|
npcName: getNpcName(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unreadCount = computed(() => {
|
const unreadCount = computed(() => {
|
||||||
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||||
})
|
})
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- 左侧:任务图标 -->
|
<!-- 左侧:任务图标 -->
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<component :is="getMissionIcon(alert.missionType)" class="h-5 w-5" :class="getMissionIconColor(alert.missionType)" />
|
<component :is="getMissionIcon(alert.missionType)" class="h-5 w-5" :class="getMissionIconColor(alert.missionType)" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 中间:主要信息 -->
|
<!-- 中间:主要信息 -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm truncate">{{ alert.npcName }}</span>
|
<span class="font-medium text-sm truncate">{{ getNpcName(alert) }}</span>
|
||||||
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs flex-shrink-0">
|
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs shrink-0">
|
||||||
{{ getMissionTypeText(alert.missionType) }}
|
{{ getMissionTypeText(alert.missionType) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,13 +52,13 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧:倒计时 -->
|
<!-- 右侧:倒计时 -->
|
||||||
<div class="flex-shrink-0 text-right">
|
<div class="shrink-0 text-right">
|
||||||
<span class="text-sm font-bold block" :class="getRemainingTimeColor(alert)">
|
<span class="text-sm font-bold block" :class="getRemainingTimeColor(alert)">
|
||||||
{{ formatRemainingTime(alert) }}
|
{{ formatRemainingTime(alert) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 未读标记 -->
|
<!-- 未读标记 -->
|
||||||
<span v-if="!alert.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0 animate-pulse" />
|
<span v-if="!alert.read" class="h-2 w-2 rounded-full bg-destructive shrink-0 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<div class="flex items-center gap-3 p-4 bg-destructive/10 rounded-lg">
|
<div class="flex items-center gap-3 p-4 bg-destructive/10 rounded-lg">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<h3 class="font-semibold text-lg">{{ selectedAlert.npcName }}</h3>
|
<h3 class="font-semibold text-lg">{{ getNpcName(selectedAlert) }}</h3>
|
||||||
<Badge :variant="getMissionBadgeVariant(selectedAlert.missionType)">
|
<Badge :variant="getMissionBadgeVariant(selectedAlert.missionType)">
|
||||||
{{ getMissionTypeText(selectedAlert.missionType) }}
|
{{ getMissionTypeText(selectedAlert.missionType) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -150,6 +150,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -157,13 +158,14 @@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
import { Siren, Eye, Sword, Shield, Globe } from 'lucide-vue-next'
|
import { Siren, Eye, Sword, Shield, Globe, Recycle } from 'lucide-vue-next'
|
||||||
import { MissionType } from '@/types/game'
|
import { MissionType } from '@/types/game'
|
||||||
import type { IncomingFleetAlert } from '@/types/game'
|
import type { IncomingFleetAlert } from '@/types/game'
|
||||||
import { formatDate, formatTime } from '@/utils/format'
|
import { formatDate, formatTime } from '@/utils/format'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
const npcStore = useNPCStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const detailDialogOpen = ref(false)
|
const detailDialogOpen = ref(false)
|
||||||
@@ -193,6 +195,31 @@
|
|||||||
.sort((a, b) => a.arrivalTime - b.arrivalTime) // 按到达时间排序
|
.sort((a, b) => a.arrivalTime - b.arrivalTime) // 按到达时间排序
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取NPC当前名称
|
||||||
|
* 优先使用当前NPC的实际名称,如果NPC不存在则尝试从旧名称中提取ID查找
|
||||||
|
*/
|
||||||
|
const getNpcName = (alert: IncomingFleetAlert): string => {
|
||||||
|
if (!npcStore.npcs?.length) return alert.npcName
|
||||||
|
|
||||||
|
// 1. 先通过 npcId 查找
|
||||||
|
if (alert.npcId) {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === alert.npcId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试从旧名称中提取ID并查找
|
||||||
|
// 旧格式如 "NPC-npc_182",新ID格式为 "npc_182"
|
||||||
|
const idMatch = alert.npcName.match(/npc_\d+/)
|
||||||
|
if (idMatch) {
|
||||||
|
const extractedId = idMatch[0]
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === extractedId)
|
||||||
|
if (npc) return npc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return alert.npcName
|
||||||
|
}
|
||||||
|
|
||||||
// 获取任务类型图标
|
// 获取任务类型图标
|
||||||
const getMissionIcon = (missionType: MissionType) => {
|
const getMissionIcon = (missionType: MissionType) => {
|
||||||
switch (missionType) {
|
switch (missionType) {
|
||||||
@@ -200,6 +227,8 @@
|
|||||||
return Eye
|
return Eye
|
||||||
case MissionType.Attack:
|
case MissionType.Attack:
|
||||||
return Sword
|
return Sword
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return Recycle
|
||||||
default:
|
default:
|
||||||
return Siren
|
return Siren
|
||||||
}
|
}
|
||||||
@@ -212,6 +241,8 @@
|
|||||||
return 'text-purple-500'
|
return 'text-purple-500'
|
||||||
case MissionType.Attack:
|
case MissionType.Attack:
|
||||||
return 'text-red-500'
|
return 'text-red-500'
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return 'text-amber-500'
|
||||||
default:
|
default:
|
||||||
return 'text-yellow-500'
|
return 'text-yellow-500'
|
||||||
}
|
}
|
||||||
@@ -229,6 +260,8 @@
|
|||||||
return t('enemyAlert.missionType.spy')
|
return t('enemyAlert.missionType.spy')
|
||||||
case MissionType.Attack:
|
case MissionType.Attack:
|
||||||
return t('enemyAlert.missionType.attack')
|
return t('enemyAlert.missionType.attack')
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return t('enemyAlert.missionType.recycle')
|
||||||
default:
|
default:
|
||||||
return t('enemyAlert.missionType.unknown')
|
return t('enemyAlert.missionType.unknown')
|
||||||
}
|
}
|
||||||
@@ -241,6 +274,8 @@
|
|||||||
return t('enemyAlert.warning.spy')
|
return t('enemyAlert.warning.spy')
|
||||||
case MissionType.Attack:
|
case MissionType.Attack:
|
||||||
return t('enemyAlert.warning.attack')
|
return t('enemyAlert.warning.attack')
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return t('enemyAlert.warning.recycle')
|
||||||
default:
|
default:
|
||||||
return t('enemyAlert.warning.unknown')
|
return t('enemyAlert.warning.unknown')
|
||||||
}
|
}
|
||||||
@@ -7,14 +7,11 @@
|
|||||||
leave-from-class="translate-y-0 opacity-100"
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
leave-to-class="-translate-y-4 opacity-0"
|
leave-to-class="-translate-y-4 opacity-0"
|
||||||
>
|
>
|
||||||
<div
|
<div v-if="isHintVisible && currentHint" class="fixed top-16 right-2 max-w-[280px] z-100 pointer-events-auto">
|
||||||
v-if="isHintVisible && currentHint"
|
|
||||||
class="fixed top-2 right-2 max-w-[280px] sm:top-4 sm:right-4 sm:max-w-xs z-50 pointer-events-auto"
|
|
||||||
>
|
|
||||||
<div class="bg-card border rounded-lg shadow-lg p-3" role="alert" aria-live="polite">
|
<div class="bg-card border rounded-lg shadow-lg p-3" role="alert" aria-live="polite">
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<component :is="getIcon(currentHint.icon)" class="h-4 w-4 text-primary flex-shrink-0" />
|
<component :is="getIcon(currentHint.icon)" class="h-4 w-4 text-primary shrink-0" />
|
||||||
<h4 class="font-medium text-sm">{{ t(currentHint.titleKey) }}</h4>
|
<h4 class="font-medium text-sm">{{ t(currentHint.titleKey) }}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
||||||
<!-- 警告图标和汇总信息 -->
|
<!-- 警告图标和汇总信息 -->
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
|
<AlertTriangle class="h-5 w-5 text-destructive shrink-0 animate-pulse" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-semibold text-destructive">
|
<p class="text-sm font-semibold text-destructive">
|
||||||
{{ getAlertSummary() }}
|
{{ getAlertSummary() }}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 查看按钮 -->
|
<!-- 查看按钮 -->
|
||||||
<Button @click="openAlertPanel" variant="outline" size="sm" class="flex-shrink-0">
|
<Button @click="openAlertPanel" variant="outline" size="sm" class="shrink-0">
|
||||||
{{ t('common.view') }}
|
{{ t('common.view') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,12 +63,14 @@
|
|||||||
|
|
||||||
// 统计各类型警报数量
|
// 统计各类型警报数量
|
||||||
const alertCounts = computed(() => {
|
const alertCounts = computed(() => {
|
||||||
const counts = { spy: 0, attack: 0, other: 0 }
|
const counts = { spy: 0, attack: 0, recycle: 0, other: 0 }
|
||||||
activeAlerts.value.forEach(alert => {
|
activeAlerts.value.forEach(alert => {
|
||||||
if (alert.missionType === MissionType.Spy) {
|
if (alert.missionType === MissionType.Spy) {
|
||||||
counts.spy++
|
counts.spy++
|
||||||
} else if (alert.missionType === MissionType.Attack) {
|
} else if (alert.missionType === MissionType.Attack) {
|
||||||
counts.attack++
|
counts.attack++
|
||||||
|
} else if (alert.missionType === MissionType.Recycle) {
|
||||||
|
counts.recycle++
|
||||||
} else {
|
} else {
|
||||||
counts.other++
|
counts.other++
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,9 @@
|
|||||||
if (alertCounts.value.spy > 0) {
|
if (alertCounts.value.spy > 0) {
|
||||||
parts.push(`${alertCounts.value.spy} ${t('enemyAlert.missionType.spy')}`)
|
parts.push(`${alertCounts.value.spy} ${t('enemyAlert.missionType.spy')}`)
|
||||||
}
|
}
|
||||||
|
if (alertCounts.value.recycle > 0) {
|
||||||
|
parts.push(`${alertCounts.value.recycle} ${t('enemyAlert.missionType.recycle')}`)
|
||||||
|
}
|
||||||
if (alertCounts.value.other > 0) {
|
if (alertCounts.value.other > 0) {
|
||||||
parts.push(`${alertCounts.value.other} ${t('enemyAlert.missionType.unknown')}`)
|
parts.push(`${alertCounts.value.other} ${t('enemyAlert.missionType.unknown')}`)
|
||||||
}
|
}
|
||||||
72
src/components/notifications/LowEnergyWarning.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="showWarning" class="bg-destructive/10 border-b border-destructive/20">
|
||||||
|
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
||||||
|
<!-- 警告图标和信息 -->
|
||||||
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<Zap class="h-5 w-5 text-destructive shrink-0 animate-pulse" />
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-destructive">
|
||||||
|
{{ t('energy.lowWarning') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ detailMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 建造电站按钮 -->
|
||||||
|
<Button @click="goToBuildSolarPlant" variant="outline" size="sm" class="shrink-0">
|
||||||
|
{{ t('energy.buildSolarPlant') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Zap } from 'lucide-vue-next'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
|
import * as officerLogic from '@/logic/officerLogic'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 获取当前星球
|
||||||
|
const planet = computed(() => gameStore.currentPlanet)
|
||||||
|
|
||||||
|
// 计算能量产量
|
||||||
|
const energyProduction = computed(() => {
|
||||||
|
if (!planet.value) return 0
|
||||||
|
const now = Date.now()
|
||||||
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||||
|
return resourceLogic.calculateEnergyProduction(planet.value, { energyProductionBonus: bonuses.energyProductionBonus })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算能量消耗
|
||||||
|
const energyConsumption = computed(() => {
|
||||||
|
if (!planet.value) return 0
|
||||||
|
return resourceLogic.calculateEnergyConsumption(planet.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示警告(电力产量 < 消耗)
|
||||||
|
const showWarning = computed(() => {
|
||||||
|
if (!planet.value) return false
|
||||||
|
return energyProduction.value < energyConsumption.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 详细消息
|
||||||
|
const detailMessage = computed(() => {
|
||||||
|
const deficit = Math.ceil(energyConsumption.value - energyProduction.value)
|
||||||
|
return t('energy.deficitDetail', { deficit: deficit.toString() })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转到建筑页面建造太阳能电站
|
||||||
|
const goToBuildSolarPlant = () => {
|
||||||
|
router.push('/buildings')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
119
src/components/notifications/OreDepositWarning.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="showWarning" class="bg-amber-500/10 border-b border-amber-500/20">
|
||||||
|
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
||||||
|
<!-- 警告图标和信息 -->
|
||||||
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<Mountain class="h-5 w-5 text-amber-500 shrink-0 animate-pulse" />
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-amber-500">
|
||||||
|
{{ warningTitle }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground truncate">
|
||||||
|
{{ detailMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看详情按钮 -->
|
||||||
|
<Button @click="goToBuildings" variant="outline" size="sm" class="shrink-0">
|
||||||
|
{{ t('common.viewDetails') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Mountain } from 'lucide-vue-next'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 获取当前星球
|
||||||
|
const planet = computed(() => gameStore.currentPlanet)
|
||||||
|
|
||||||
|
// 检查各资源的矿脉状态
|
||||||
|
const depositStatus = computed(() => {
|
||||||
|
if (!planet.value || planet.value.isMoon) return null
|
||||||
|
|
||||||
|
const deposits = planet.value.oreDeposits
|
||||||
|
if (!deposits) return null
|
||||||
|
|
||||||
|
const resources = ['metal', 'crystal', 'deuterium'] as const
|
||||||
|
const warnings: { type: string; depleted: boolean; percentage: number }[] = []
|
||||||
|
|
||||||
|
for (const resource of resources) {
|
||||||
|
const isDepleted = oreDepositLogic.isDepositDepleted(deposits, resource)
|
||||||
|
const isWarning = oreDepositLogic.isDepositWarning(deposits, resource)
|
||||||
|
const percentage = oreDepositLogic.getDepositPercentage(deposits, resource)
|
||||||
|
|
||||||
|
if (isDepleted || isWarning) {
|
||||||
|
warnings.push({
|
||||||
|
type: resource,
|
||||||
|
depleted: isDepleted,
|
||||||
|
percentage: Math.round(percentage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings.length > 0 ? warnings : null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示警告
|
||||||
|
const showWarning = computed(() => {
|
||||||
|
return depositStatus.value !== null && depositStatus.value.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取资源名称翻译
|
||||||
|
const getResourceName = (type: string): string => {
|
||||||
|
const resourceNames: Record<string, string> = {
|
||||||
|
metal: t('resources.metal'),
|
||||||
|
crystal: t('resources.crystal'),
|
||||||
|
deuterium: t('resources.deuterium')
|
||||||
|
}
|
||||||
|
return resourceNames[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 警告标题
|
||||||
|
const warningTitle = computed(() => {
|
||||||
|
if (!depositStatus.value) return ''
|
||||||
|
|
||||||
|
const hasDepleted = depositStatus.value.some(s => s.depleted)
|
||||||
|
if (hasDepleted) {
|
||||||
|
return t('oreDeposit.depletedWarning')
|
||||||
|
}
|
||||||
|
return t('oreDeposit.lowWarning')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 详细消息
|
||||||
|
const detailMessage = computed(() => {
|
||||||
|
if (!depositStatus.value) return ''
|
||||||
|
|
||||||
|
const depletedResources = depositStatus.value.filter(s => s.depleted).map(s => getResourceName(s.type))
|
||||||
|
|
||||||
|
const warningResources = depositStatus.value.filter(s => !s.depleted).map(s => `${getResourceName(s.type)} (${s.percentage}%)`)
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
if (depletedResources.length > 0) {
|
||||||
|
parts.push(t('oreDeposit.depletedResources', { resources: depletedResources.join(', ') }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warningResources.length > 0) {
|
||||||
|
parts.push(t('oreDeposit.lowResources', { resources: warningResources.join(', ') }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' | ')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转到建筑页面查看详情
|
||||||
|
const goToBuildings = () => {
|
||||||
|
router.push('/galaxy')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
317
src/components/notifications/QueueNotifications.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<template>
|
||||||
|
<Popover v-model:open="isOpen">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button data-tutorial="queue-button" variant="outline" size="icon" class="relative">
|
||||||
|
<ListOrdered class="h-4 w-4" />
|
||||||
|
<Badge
|
||||||
|
v-if="totalQueueCount > 0"
|
||||||
|
variant="default"
|
||||||
|
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
|
||||||
|
>
|
||||||
|
{{ totalQueueCount }}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-96 p-0" align="end">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="font-semibold">{{ t('queue.title') }} ({{ totalQueueCount }})</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
|
<TabsList class="w-full grid grid-cols-6 h-auto min-h-9 rounded-none border-b bg-transparent">
|
||||||
|
<TabsTrigger
|
||||||
|
v-for="tab in tabConfig"
|
||||||
|
:key="tab.value"
|
||||||
|
:value="tab.value"
|
||||||
|
class="text-xs px-1 py-1.5 flex items-center justify-center gap-0.5 whitespace-nowrap data-[state=active]:bg-muted"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ t(`queue.tabs.${tab.value}`) }}</span>
|
||||||
|
<Badge v-if="tab.items.length > 0" variant="secondary" class="shrink-0 h-4 px-1 text-[10px]">
|
||||||
|
{{ tab.items.length }}
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<ScrollArea class="h-[420px]">
|
||||||
|
<TabsContent v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="mt-0">
|
||||||
|
<Empty v-if="tab.items.length === 0" class="border-0">
|
||||||
|
<EmptyContent>
|
||||||
|
<Inbox class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ tab.isWaiting ? t('queue.waitingEmpty') : t('queue.empty') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
<div v-else class="divide-y p-4 space-y-3">
|
||||||
|
<!-- 等待队列项 -->
|
||||||
|
<template v-if="tab.isWaiting">
|
||||||
|
<div v-for="item in tab.items" :key="item.id" class="space-y-1.5">
|
||||||
|
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
||||||
|
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
||||||
|
<div class="h-2 w-2 rounded-full shrink-0" :class="getStatusDotClass(item)" />
|
||||||
|
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
||||||
|
<span class="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
{{
|
||||||
|
item.type === 'ship' || item.type === 'defense'
|
||||||
|
? `→ ${t('queue.quantity')} ${item.quantity}`
|
||||||
|
: item.type === 'demolish'
|
||||||
|
? `→ ${t('queue.demolishing')}`
|
||||||
|
: `→ ${t('queue.level')} ${item.targetLevel}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||||
|
<span
|
||||||
|
class="text-[10px] sm:text-xs whitespace-nowrap"
|
||||||
|
:class="isWaitingItemResourcesReady(item as WaitingQueueItem) ? 'text-green-500' : 'text-yellow-500'"
|
||||||
|
>
|
||||||
|
{{ isWaitingItemResourcesReady(item as WaitingQueueItem) ? t('queue.resourcesReady') : t('queue.waitingResources') }}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
||||||
|
@click.stop="handleCancel(item)"
|
||||||
|
>
|
||||||
|
{{ t('queue.remove') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 预估成本显示 -->
|
||||||
|
<div class="flex gap-2 text-[10px] text-muted-foreground ml-4">
|
||||||
|
<span v-if="getWaitingItemCost(item as WaitingQueueItem).metal > 0">
|
||||||
|
{{ t('resources.metal') }}: {{ formatNumber(getWaitingItemCost(item as WaitingQueueItem).metal) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="getWaitingItemCost(item as WaitingQueueItem).crystal > 0">
|
||||||
|
{{ t('resources.crystal') }}: {{ formatNumber(getWaitingItemCost(item as WaitingQueueItem).crystal) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="getWaitingItemCost(item as WaitingQueueItem).deuterium > 0">
|
||||||
|
{{ t('resources.deuterium') }}: {{ formatNumber(getWaitingItemCost(item as WaitingQueueItem).deuterium) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 正式队列项 -->
|
||||||
|
<template v-else>
|
||||||
|
<div v-for="item in tab.items" :key="item.id" class="space-y-1.5">
|
||||||
|
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
||||||
|
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
||||||
|
<div class="h-2 w-2 rounded-full animate-pulse shrink-0" :class="getStatusDotClass(item)" />
|
||||||
|
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
||||||
|
<span class="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
{{
|
||||||
|
item.type === 'ship' || item.type === 'defense'
|
||||||
|
? `→ ${t('queue.quantity')} ${item.quantity}`
|
||||||
|
: item.type === 'demolish'
|
||||||
|
? `→ ${t('queue.demolishing')}`
|
||||||
|
: `→ ${t('queue.level')} ${item.targetLevel}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||||
|
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
|
||||||
|
{{ formatTime(getRemainingTime(item as BuildQueueItem)) }}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
||||||
|
@click.stop="handleCancel(item)"
|
||||||
|
>
|
||||||
|
{{ t('queue.cancel') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Progress :model-value="getQueueProgress(item as BuildQueueItem)" class="h-1.5" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</ScrollArea>
|
||||||
|
</Tabs>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onUnmounted, watch } from 'vue'
|
||||||
|
import { ListOrdered, Inbox } from 'lucide-vue-next'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { formatTime, formatNumber } from '@/utils/format'
|
||||||
|
import type { BuildQueueItem, WaitingQueueItem, BuildingType, ShipType, DefenseType, TechnologyType, Resources } from '@/types/game'
|
||||||
|
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
|
||||||
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { BUILDINGS, SHIPS, DEFENSES, TECHNOLOGIES } = useGameConfig()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 响应式时间戳,用于驱动时间和进度的动态更新
|
||||||
|
const currentTime = ref(Date.now())
|
||||||
|
let timerInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
// 当弹窗打开时启动计时器,关闭时停止
|
||||||
|
watch(isOpen, open => {
|
||||||
|
if (open) {
|
||||||
|
// 启动每秒更新的计时器
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
currentTime.value = Date.now()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// 停止计时器
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
timerInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理计时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
timerInterval = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前星球的建造队列
|
||||||
|
const buildQueue = computed(() => {
|
||||||
|
return gameStore.currentPlanet?.buildQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取研究队列
|
||||||
|
const researchQueue = computed(() => {
|
||||||
|
return gameStore.player.researchQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前星球的建造等待队列
|
||||||
|
const buildWaitingQueue = computed(() => {
|
||||||
|
return gameStore.currentPlanet?.waitingBuildQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取研究等待队列
|
||||||
|
const researchWaitingQueue = computed(() => {
|
||||||
|
return gameStore.player.waitingResearchQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 合并所有等待队列
|
||||||
|
const allWaitingQueue = computed(() => {
|
||||||
|
return [...buildWaitingQueue.value, ...researchWaitingQueue.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 总队列数量(包括等待队列)
|
||||||
|
const totalQueueCount = computed(() => {
|
||||||
|
return buildQueue.value.length + researchQueue.value.length + allWaitingQueue.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标签页配置(用于循环渲染)
|
||||||
|
const tabConfig = computed(() => [
|
||||||
|
{ value: 'all', items: [...buildQueue.value, ...researchQueue.value], isWaiting: false },
|
||||||
|
{ value: 'buildings', items: buildQueue.value.filter(item => item.type === 'building' || item.type === 'demolish'), isWaiting: false },
|
||||||
|
{ value: 'research', items: researchQueue.value, isWaiting: false },
|
||||||
|
{ value: 'ships', items: buildQueue.value.filter(item => item.type === 'ship'), isWaiting: false },
|
||||||
|
{ value: 'defense', items: buildQueue.value.filter(item => item.type === 'defense'), isWaiting: false },
|
||||||
|
{ value: 'waiting', items: allWaitingQueue.value, isWaiting: true }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 获取队列项名称
|
||||||
|
const getItemName = (item: BuildQueueItem | WaitingQueueItem): string => {
|
||||||
|
if (item.type === 'building' || item.type === 'demolish') {
|
||||||
|
return BUILDINGS.value[item.itemType as BuildingType].name
|
||||||
|
} else if (item.type === 'ship') {
|
||||||
|
return SHIPS.value[item.itemType as ShipType].name
|
||||||
|
} else if (item.type === 'defense') {
|
||||||
|
return DEFENSES.value[item.itemType as DefenseType].name
|
||||||
|
} else if (item.type === 'technology') {
|
||||||
|
return TECHNOLOGIES.value[item.itemType as TechnologyType].name
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是等待队列项
|
||||||
|
const isWaitingItem = (item: BuildQueueItem | WaitingQueueItem): item is WaitingQueueItem => {
|
||||||
|
return 'addedTime' in item && 'priority' in item
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取等待队列项的预估成本
|
||||||
|
const getWaitingItemCost = (item: WaitingQueueItem): Resources => {
|
||||||
|
return waitingQueueLogic.calculateWaitingItemCost(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查等待队列项资源是否足够
|
||||||
|
const isWaitingItemResourcesReady = (item: WaitingQueueItem): boolean => {
|
||||||
|
const cost = getWaitingItemCost(item)
|
||||||
|
const resources = gameStore.currentPlanet?.resources
|
||||||
|
if (!resources) return false
|
||||||
|
return resourceLogic.checkResourcesAvailable(resources, cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剩余时间(使用响应式 currentTime 确保动态更新)
|
||||||
|
const getRemainingTime = (item: BuildQueueItem): number => {
|
||||||
|
return Math.max(0, Math.floor((item.endTime - currentTime.value) / 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取队列进度(使用响应式 currentTime 确保动态更新)
|
||||||
|
const getQueueProgress = (item: BuildQueueItem): number => {
|
||||||
|
const elapsed = currentTime.value - item.startTime
|
||||||
|
const total = item.endTime - item.startTime
|
||||||
|
if (total <= 0) return 100
|
||||||
|
return Math.max(0, Math.min(100, (elapsed / total) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的取消处理
|
||||||
|
const handleCancel = (item: BuildQueueItem | WaitingQueueItem) => {
|
||||||
|
// 检查是否是等待队列项
|
||||||
|
if (isWaitingItem(item)) {
|
||||||
|
handleRemoveFromWaiting(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventName: string
|
||||||
|
if (item.type === 'building' || item.type === 'ship' || item.type === 'defense' || item.type === 'demolish') {
|
||||||
|
eventName = 'cancel-build'
|
||||||
|
} else if (item.type === 'technology') {
|
||||||
|
eventName = 'cancel-research'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new CustomEvent(eventName, { detail: item.id })
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从等待队列移除
|
||||||
|
const handleRemoveFromWaiting = (item: WaitingQueueItem) => {
|
||||||
|
const planet = gameStore.currentPlanet
|
||||||
|
if (!planet) return
|
||||||
|
|
||||||
|
if (item.type === 'technology') {
|
||||||
|
// 从研究等待队列移除
|
||||||
|
waitingQueueLogic.removeFromResearchWaitingQueue(gameStore.player, item.id)
|
||||||
|
} else {
|
||||||
|
// 从建筑等待队列移除
|
||||||
|
waitingQueueLogic.removeFromBuildWaitingQueue(planet, item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态指示点颜色
|
||||||
|
const getStatusDotClass = (item: BuildQueueItem | WaitingQueueItem): string => {
|
||||||
|
// 等待队列项根据资源是否足够显示不同颜色
|
||||||
|
if (isWaitingItem(item)) {
|
||||||
|
return isWaitingItemResourcesReady(item) ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
|
}
|
||||||
|
if (item.type === 'demolish') return 'bg-destructive'
|
||||||
|
if (item.type === 'technology') return 'bg-blue-500'
|
||||||
|
return 'bg-green-500'
|
||||||
|
}
|
||||||
|
</script>
|
||||||