mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
feat: 新增多语言README并优化文档结构
新增德语、俄语、韩语、繁体中文多语言README,英文与简体中文README同步优化,统一下载链接与徽章样式,完善多语言入口。提升国际化支持与文档可读性。
This commit is contained in:
184
README-DE.md
Normal file
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>
|
||||
55
README-EN.md
55
README-EN.md
@@ -5,13 +5,28 @@
|
||||
|
||||
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://vuejs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](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>
|
||||
|
||||
@@ -49,28 +64,6 @@ OGame Vue TS is a single-player, browser-based space strategy game inspired by t
|
||||
|
||||
## 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
|
||||
|
||||
- [Node.js](https://nodejs.org/) (version 18 or higher recommended)
|
||||
@@ -111,12 +104,12 @@ pnpm preview
|
||||
|
||||
## Available Languages
|
||||
|
||||
- 🇺🇸 English
|
||||
- 🇨🇳 简体中文 (Simplified Chinese)
|
||||
- 🇹🇼 繁體中文 (Traditional Chinese)
|
||||
- 🇩🇪 Deutsch (German)
|
||||
- 🇷🇺 Русский (Russian)
|
||||
- 🇰🇷 한국어 (Korean)
|
||||
- 简体中文 (Simplified Chinese)
|
||||
- 繁體中文 (Traditional Chinese)
|
||||
- English
|
||||
- Deutsch (German)
|
||||
- Русский (Russian)
|
||||
- 한국어 (Korean)
|
||||
|
||||
## Game Features
|
||||
|
||||
@@ -187,7 +180,7 @@ This project is not affiliated with, endorsed by, or connected to Gameforge AG o
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
Made with ❤️ by Jun Qian
|
||||
Made with ❤️ by <a href="https://github.com/setube">setube</a>
|
||||
<br>
|
||||
© 2025 - All rights reserved (except those granted by CC BY-NC 4.0 License)
|
||||
</div>
|
||||
|
||||
184
README-KO.md
Normal file
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
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
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>
|
||||
52
README.md
52
README.md
@@ -12,7 +12,21 @@
|
||||
[](https://vitejs.dev/)
|
||||
[](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>
|
||||
|
||||
@@ -50,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 或更高版本)
|
||||
@@ -112,12 +104,12 @@ pnpm preview
|
||||
|
||||
## 支持的语言
|
||||
|
||||
- 🇺🇸 English (英语)
|
||||
- 🇨🇳 简体中文
|
||||
- 🇹🇼 繁體中文
|
||||
- 🇩🇪 Deutsch (德语)
|
||||
- 🇷🇺 Русский (俄语)
|
||||
- 🇰🇷 한국어 (韩语)
|
||||
- 简体中文
|
||||
- 繁體中文
|
||||
- English (英语)
|
||||
- Deutsch (德语)
|
||||
- Русский (俄语)
|
||||
- 한국어 (韩语)
|
||||
|
||||
## 游戏特性
|
||||
|
||||
@@ -186,7 +178,7 @@ pnpm preview
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
用 ❤️ 制作,作者:谦君
|
||||
用 ❤️ 制作,作者:<a href="https://github.com/setube">setube</a>
|
||||
<br>
|
||||
© 2025 - 保留所有权利(除 CC BY-NC 4.0 许可证授予的权利外)
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ android {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
if (abi != null) {
|
||||
output.versionCodeOverride = abiVersionCode[abi] * 1000 + defaultConfig.versionCode
|
||||
output.outputFileName = "OGame-${defaultConfig.versionName}-${abi}.APK"
|
||||
output.outputFileName = "OGame Vue Ts-${abi}.APK"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">OGame</string>
|
||||
<string name="title_activity_main">OGame</string>
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CapacitorConfig } from '@capacitor/cli'
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'games.wenzi.ogame',
|
||||
appName: 'OGame',
|
||||
appName: 'OGame Vue Ts',
|
||||
webDir: 'docs',
|
||||
server: {
|
||||
androidScheme: 'https'
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"version": "1.4.0",
|
||||
"buildDate": "2025/12/20 04:05:08",
|
||||
"buildDate": "2025/12/23 19:38:12",
|
||||
"main": "dist-electron/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad",
|
||||
"build": {
|
||||
"appId": "games.wenzi.ogame",
|
||||
"productName": "OGame",
|
||||
"productName": "OGame Vue Ts",
|
||||
"directories": {
|
||||
"output": "pkg"
|
||||
},
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -693,8 +693,8 @@ packages:
|
||||
resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: https://github.com/electron/node-gyp.git, type: git}
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
|
||||
version: 10.2.0-electron.1
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
@@ -4680,7 +4680,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
exponential-backoff: 3.1.3
|
||||
@@ -4717,7 +4717,7 @@ snapshots:
|
||||
|
||||
'@electron/rebuild@3.7.0':
|
||||
dependencies:
|
||||
'@electron/node-gyp': git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2
|
||||
'@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2
|
||||
'@malept/cross-spawn-promise': 2.0.0
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
|
||||
297
src/App.vue
297
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"
|
||||
>
|
||||
<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="text-[10px] text-muted-foreground uppercase tracking-wider mb-0.5">
|
||||
{{ t('planet.currentPlanet') }}
|
||||
</div>
|
||||
<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">
|
||||
{{ t('planet.moon') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</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>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-0" side="bottom" align="start">
|
||||
@@ -61,14 +61,17 @@
|
||||
size="sm"
|
||||
>
|
||||
<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 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
|
||||
variant="ghost"
|
||||
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)"
|
||||
:title="t('planet.renamePlanet')"
|
||||
>
|
||||
@@ -78,9 +81,6 @@
|
||||
{{ t('planet.moon') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
[{{ p.position.galaxy }}:{{ p.position.system }}:{{ p.position.position }}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -233,23 +233,23 @@
|
||||
class="resource-bar flex items-center gap-3 sm:gap-6 justify-start sm:justify-center"
|
||||
:class="[resourceBarExpanded ? 'hidden' : 'overflow-x-auto']"
|
||||
>
|
||||
<div
|
||||
v-for="resourceType in resourceTypes"
|
||||
:key="resourceType.key"
|
||||
class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0"
|
||||
>
|
||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 shrink-0">
|
||||
<ResourceIcon :type="resourceType.key" size="md" />
|
||||
<div class="min-w-0">
|
||||
<!-- 电力显示净产量和效率 -->
|
||||
<!-- 电力显示:当前储量/最大容量,净产量/小时 -->
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
<p
|
||||
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'"
|
||||
>
|
||||
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
|
||||
</p>
|
||||
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
|
||||
{{ formatNumber(production?.energy || 0) }} / {{ formatNumber(energyConsumption) }}
|
||||
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(Math.round(netEnergy / 60)) }}/{{ t('resources.perMinute') }}
|
||||
</p>
|
||||
</template>
|
||||
<!-- 其他资源统一显示:当前值/容量 -->
|
||||
@@ -271,7 +271,7 @@
|
||||
</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">
|
||||
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
||||
@@ -306,16 +306,21 @@
|
||||
<span class="text-xs font-medium text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}</span>
|
||||
</div>
|
||||
<div class="space-y-0.5 text-center">
|
||||
<!-- 电力显示净产量和效率 -->
|
||||
<!-- 电力显示:当前储量,容量,净产量/分钟 -->
|
||||
<template v-if="resourceType.key === 'energy'">
|
||||
<p
|
||||
class="text-sm font-semibold"
|
||||
:class="netEnergy >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||
>
|
||||
{{ netEnergy >= 0 ? '+' : '' }}{{ formatNumber(netEnergy) }}
|
||||
<p class="text-sm font-semibold" :class="getResourceColor(planet.resources.energy, capacity?.energy || Infinity)">
|
||||
{{ formatNumber(planet.resources.energy) }}
|
||||
</p>
|
||||
<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>
|
||||
</template>
|
||||
<!-- 其他资源统一显示:当前值/容量 -->
|
||||
@@ -347,6 +352,9 @@
|
||||
<!-- 低电量警告 -->
|
||||
<LowEnergyWarning />
|
||||
|
||||
<!-- 矿脉储量警告 -->
|
||||
<OreDepositWarning />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1">
|
||||
<Transition name="page" mode="out-in">
|
||||
@@ -450,6 +458,22 @@
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -467,11 +491,12 @@
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
|
||||
import LowEnergyWarning from '@/components/LowEnergyWarning.vue'
|
||||
import DiplomaticNotifications from '@/components/DiplomaticNotifications.vue'
|
||||
import EnemyAlertNotifications from '@/components/EnemyAlertNotifications.vue'
|
||||
import QueueNotifications from '@/components/QueueNotifications.vue'
|
||||
import IncomingFleetAlerts from '@/components/notifications/IncomingFleetAlerts.vue'
|
||||
import LowEnergyWarning from '@/components/notifications/LowEnergyWarning.vue'
|
||||
import OreDepositWarning from '@/components/notifications/OreDepositWarning.vue'
|
||||
import DiplomaticNotifications from '@/components/notifications/DiplomaticNotifications.vue'
|
||||
import EnemyAlertNotifications from '@/components/notifications/EnemyAlertNotifications.vue'
|
||||
import QueueNotifications from '@/components/notifications/QueueNotifications.vue'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -486,7 +511,7 @@
|
||||
SidebarProvider,
|
||||
SidebarTrigger
|
||||
} from '@/components/ui/sidebar'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -497,10 +522,10 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import DetailDialog from '@/components/DetailDialog.vue'
|
||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||
import HintToast from '@/components/HintToast.vue'
|
||||
import BackToTop from '@/components/BackToTop.vue'
|
||||
import DetailDialog from '@/components/dialogs/DetailDialog.vue'
|
||||
import UpdateDialog from '@/components/dialogs/UpdateDialog.vue'
|
||||
import HintToast from '@/components/notifications/HintToast.vue'
|
||||
import BackToTop from '@/components/common/BackToTop.vue'
|
||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game'
|
||||
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
||||
@@ -530,7 +555,9 @@
|
||||
ChevronUp,
|
||||
Handshake,
|
||||
Pencil,
|
||||
Trophy
|
||||
Trophy,
|
||||
Crown,
|
||||
Scroll
|
||||
} from 'lucide-vue-next'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
@@ -544,6 +571,9 @@
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
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 { toast } from 'vue-sonner'
|
||||
import { migrateGameData } from '@/utils/migration'
|
||||
@@ -596,6 +626,9 @@
|
||||
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 }> = {
|
||||
'/research': { building: BuildingType.ResearchLab, level: 1 },
|
||||
@@ -632,6 +665,8 @@
|
||||
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
||||
{ 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.settings')), path: '/settings', icon: Settings },
|
||||
// GM菜单在启用GM模式时显示
|
||||
@@ -732,7 +767,7 @@
|
||||
|
||||
if (!settings.types[typeKey]) return
|
||||
|
||||
// browser
|
||||
// 浏览器通知
|
||||
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
|
||||
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
|
||||
if (!shouldSuppress) {
|
||||
@@ -740,12 +775,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
// toast
|
||||
// 页面内 toast 通知
|
||||
if (settings.inApp) {
|
||||
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 = () => {
|
||||
if (confirmDialogAction.value) {
|
||||
confirmDialogAction.value()
|
||||
@@ -757,6 +821,20 @@
|
||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||
if (!shouldInit) {
|
||||
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)
|
||||
@@ -802,7 +880,7 @@
|
||||
// 检查军官过期
|
||||
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.fleetMissions.forEach(mission => {
|
||||
@@ -845,23 +923,40 @@
|
||||
// 检查成就解锁
|
||||
checkAchievementUnlocks()
|
||||
|
||||
// 检查战役任务进度
|
||||
if (gameStore.player.campaignProgress) {
|
||||
campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs)
|
||||
}
|
||||
|
||||
// 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
||||
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
|
||||
if (eliminatedNpcIds.length > 0) {
|
||||
// 从universeStore中移除被消灭NPC的星球数据
|
||||
// 从universeStore中移除被消灭NPC的星球数据,并收集需要清理的任务ID
|
||||
const missionIdsToRemove: string[] = []
|
||||
eliminatedNpcIds.forEach(npcId => {
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
if (npc && npc.planets) {
|
||||
if (npc) {
|
||||
// 遍历NPC的所有星球,从universeStore中删除
|
||||
npc.planets.forEach(planet => {
|
||||
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
|
||||
if (universeStore.planets[planetKey]) {
|
||||
delete universeStore.planets[planetKey]
|
||||
}
|
||||
})
|
||||
if (npc.planets) {
|
||||
npc.planets.forEach(planet => {
|
||||
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
|
||||
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
|
||||
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
|
||||
}
|
||||
@@ -1251,11 +1346,59 @@
|
||||
// 不是玩家星球,从宇宙地图中移除
|
||||
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) {
|
||||
// 处理远征任务
|
||||
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'
|
||||
@@ -1268,32 +1411,35 @@
|
||||
|
||||
// 根据事件类型生成不同的报告消息
|
||||
let reportMessage = ''
|
||||
let reportDetails: Record<string, unknown> = {}
|
||||
let reportDetails: Record<string, unknown> = {
|
||||
// 保存探险区域信息
|
||||
expeditionZone: mission.expeditionZone
|
||||
}
|
||||
|
||||
switch (expeditionResult.eventType) {
|
||||
case 'resources':
|
||||
reportMessage = t('missionReports.expeditionResources')
|
||||
reportDetails = { foundResources: expeditionResult.resources }
|
||||
reportDetails.foundResources = expeditionResult.resources
|
||||
break
|
||||
case 'darkMatter':
|
||||
reportMessage = t('missionReports.expeditionDarkMatter')
|
||||
reportDetails = { foundResources: expeditionResult.resources }
|
||||
reportDetails.foundResources = expeditionResult.resources
|
||||
break
|
||||
case 'fleet':
|
||||
reportMessage = t('missionReports.expeditionFleet')
|
||||
reportDetails = { foundFleet: expeditionResult.fleet }
|
||||
reportDetails.foundFleet = expeditionResult.fleet
|
||||
break
|
||||
case 'pirates':
|
||||
reportMessage = expeditionResult.fleetLost
|
||||
? t('missionReports.expeditionPiratesAttack')
|
||||
: t('missionReports.expeditionPiratesEscaped')
|
||||
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
||||
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
|
||||
break
|
||||
case 'aliens':
|
||||
reportMessage = expeditionResult.fleetLost
|
||||
? t('missionReports.expeditionAliensAttack')
|
||||
: t('missionReports.expeditionAliensEscaped')
|
||||
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
||||
if (expeditionResult.fleetLost) reportDetails.fleetLost = expeditionResult.fleetLost
|
||||
break
|
||||
default:
|
||||
reportMessage = t('missionReports.expeditionNothing')
|
||||
@@ -1656,7 +1802,7 @@
|
||||
|
||||
npcMap.set(planet.ownerId, {
|
||||
id: planet.ownerId,
|
||||
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
||||
name: generateNPCName(planet.ownerId, gameStore.locale),
|
||||
planets: [],
|
||||
technologies: {}, // 初始化空科技树
|
||||
difficulty: 'medium' as const, // 默认中等难度
|
||||
@@ -1700,6 +1846,11 @@
|
||||
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
||||
}
|
||||
|
||||
// 确保所有NPC都有AI类型(修复旧版本保存的数据)
|
||||
if (npcStore.npcs.length > 0) {
|
||||
npcGrowthLogic.ensureAllNPCsAIType(npcStore.npcs)
|
||||
}
|
||||
|
||||
// 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据)
|
||||
if (npcStore.npcs.length > 0) {
|
||||
const now = Date.now()
|
||||
@@ -2099,6 +2250,15 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 检测旧格式 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 }) => {
|
||||
@@ -2155,6 +2315,25 @@
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -45,32 +45,7 @@
|
||||
--ring: oklch(0.442 0.017 285.786);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--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);
|
||||
}
|
||||
/* Theme variables are defined in style.css */
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@@ -93,3 +68,23 @@ html.dark {
|
||||
html.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,201 +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
|
||||
if (total <= 0) return 100
|
||||
return Math.max(0, 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
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
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
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>
|
||||
<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="flex justify-center">
|
||||
<div class="rounded-full bg-muted p-2">
|
||||
@@ -20,8 +24,8 @@
|
||||
<AlertDialogDescription>
|
||||
<div class="space-y-2">
|
||||
<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" />
|
||||
<X v-else :size="16" class="text-red-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 shrink-0" />
|
||||
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -166,6 +166,56 @@
|
||||
</Table>
|
||||
</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">
|
||||
<Card>
|
||||
@@ -393,15 +443,20 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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 * as buildingLogic from '@/logic/buildingLogic'
|
||||
import * as researchLogic from '@/logic/researchLogic'
|
||||
import * as pointsLogic from '@/logic/pointsLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
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 { 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 gameStore = useGameStore()
|
||||
@@ -442,6 +497,11 @@
|
||||
return currentPlanet.value.buildings['researchLab'] || 0
|
||||
})
|
||||
|
||||
// 获取能量科技等级(用于研究时间计算)
|
||||
const energyTechLevel = computed(() => {
|
||||
return gameStore.player.technologies['energyTechnology'] || 0
|
||||
})
|
||||
|
||||
// 翻译键(转换为复数形式)
|
||||
const typeKey = computed(() => {
|
||||
const typeMap = {
|
||||
@@ -472,11 +532,79 @@
|
||||
})
|
||||
|
||||
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
|
||||
const buildingType = props.itemType as 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(() => {
|
||||
if (props.type !== 'building') return false
|
||||
const buildingType = props.itemType as BuildingType
|
||||
@@ -717,7 +845,8 @@
|
||||
}),
|
||||
darkMatterCollector: lvl => ({
|
||||
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 => ({
|
||||
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
|
||||
@@ -726,25 +855,39 @@
|
||||
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
|
||||
}),
|
||||
shipyard: lvl => ({
|
||||
fleetStorage: 1000 * lvl
|
||||
fleetStorage: 1000 * lvl,
|
||||
consumption: Math.floor(8 * lvl * Math.pow(1.1, lvl))
|
||||
}),
|
||||
hangar: lvl => ({
|
||||
fleetStorage: 500 * lvl
|
||||
}),
|
||||
terraformer: () => ({
|
||||
spaceBonus: 30
|
||||
}),
|
||||
lunarBase: () => ({
|
||||
spaceBonus: 30
|
||||
}),
|
||||
roboticsFactory: lvl => ({
|
||||
buildSpeedBonus: lvl
|
||||
buildSpeedBonus: lvl,
|
||||
consumption: Math.floor(5 * lvl * Math.pow(1.1, lvl))
|
||||
}),
|
||||
naniteFactory: lvl => ({
|
||||
buildSpeedBonus: lvl * 2
|
||||
buildSpeedBonus: lvl * 2,
|
||||
consumption: Math.floor(20 * lvl * Math.pow(1.15, 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,
|
||||
level - 1,
|
||||
activeBonuses.value.researchSpeedBonus,
|
||||
researchLabLevel.value
|
||||
researchLabLevel.value,
|
||||
energyTechLevel.value
|
||||
)
|
||||
|
||||
let researchSpeedBonus = 0
|
||||
@@ -3,23 +3,17 @@
|
||||
<PopoverTrigger as-child>
|
||||
<span class="cursor-pointer underline decoration-dotted underline-offset-4 touch-manipulation">{{ formatNumber(value, 1) }}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-2" side="top" align="center">
|
||||
<p class="font-mono text-sm">{{ formattedValue }}</p>
|
||||
<PopoverContent class="w-auto p-2 z-100" side="top" align="center">
|
||||
<p class="font-mono text-sm">{{ props.value.toLocaleString() }}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
value: number
|
||||
}>()
|
||||
|
||||
// 完整格式化的数字(带千位分隔符)
|
||||
const formattedValue = computed(() => {
|
||||
return props.value.toLocaleString()
|
||||
})
|
||||
</script>
|
||||
628
src/components/dialogs/BattleAnimationPlayer.vue
Normal file
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
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 { useDetailDialogStore } from '@/stores/detailDialogStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import ItemDetailView from './ItemDetailView.vue'
|
||||
import ItemDetailView from '@/components/common/ItemDetailView.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDetailDialogStore()
|
||||
346
src/components/dialogs/MissionReportDialog.vue
Normal file
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
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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import pkg from '../../package.json'
|
||||
import pkg from '../../../package.json'
|
||||
|
||||
// 双向绑定 open 状态
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
147
src/components/dialogs/SpiedNotificationDialog.vue
Normal file
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
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>
|
||||
<Dialog :open="open" @update:open="$emit('update:open', $event)">
|
||||
<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>
|
||||
<DialogDescription>{{ t('settings.updateAvailable') }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown" />
|
||||
</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)">
|
||||
{{ t('common.cancel') }}
|
||||
</Button>
|
||||
@@ -36,14 +36,14 @@
|
||||
>
|
||||
<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)" />
|
||||
</div>
|
||||
<!-- 中间:主要信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm truncate">{{ report.npcName }}</span>
|
||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs flex-shrink-0">
|
||||
<span class="font-medium text-sm truncate">{{ getNpcName(report) }}</span>
|
||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs shrink-0">
|
||||
{{ getStatusText(report.newStatus) }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- 右侧:好感度变化和时间 -->
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<div class="shrink-0 text-right">
|
||||
<span
|
||||
class="text-sm font-bold block"
|
||||
:class="report.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||
@@ -64,7 +64,7 @@
|
||||
</span>
|
||||
</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>
|
||||
@@ -100,7 +100,7 @@
|
||||
<div class="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex-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)">
|
||||
{{ getStatusText(selectedReport.newStatus) }}
|
||||
</Badge>
|
||||
@@ -117,7 +117,7 @@
|
||||
<p class="text-sm p-3 bg-muted/30 rounded-md">
|
||||
{{
|
||||
selectedReport.messageKey && selectedReport.messageParams
|
||||
? t(selectedReport.messageKey, selectedReport.messageParams)
|
||||
? t(selectedReport.messageKey, getMessageParams(selectedReport))
|
||||
: selectedReport.message
|
||||
}}
|
||||
</p>
|
||||
@@ -195,6 +195,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -209,6 +210,7 @@
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const isOpen = ref(false)
|
||||
const detailDialogOpen = ref(false)
|
||||
@@ -218,6 +220,42 @@
|
||||
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(() => {
|
||||
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||
})
|
||||
@@ -36,14 +36,14 @@
|
||||
>
|
||||
<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)" />
|
||||
</div>
|
||||
<!-- 中间:主要信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm truncate">{{ alert.npcName }}</span>
|
||||
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs flex-shrink-0">
|
||||
<span class="font-medium text-sm truncate">{{ getNpcName(alert) }}</span>
|
||||
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs shrink-0">
|
||||
{{ getMissionTypeText(alert.missionType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -52,13 +52,13 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- 右侧:倒计时 -->
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<div class="shrink-0 text-right">
|
||||
<span class="text-sm font-bold block" :class="getRemainingTimeColor(alert)">
|
||||
{{ formatRemainingTime(alert) }}
|
||||
</span>
|
||||
</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>
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="flex items-center gap-3 p-4 bg-destructive/10 rounded-lg">
|
||||
<div class="flex-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)">
|
||||
{{ getMissionTypeText(selectedAlert.missionType) }}
|
||||
</Badge>
|
||||
@@ -150,6 +150,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -164,6 +165,7 @@
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
const isOpen = ref(false)
|
||||
const detailDialogOpen = ref(false)
|
||||
@@ -193,6 +195,31 @@
|
||||
.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) => {
|
||||
switch (missionType) {
|
||||
@@ -7,14 +7,11 @@
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="-translate-y-4 opacity-0"
|
||||
>
|
||||
<div
|
||||
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 v-if="isHintVisible && currentHint" class="fixed top-16 right-2 max-w-[280px] z-100 pointer-events-auto">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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">
|
||||
<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">
|
||||
<p class="text-sm font-semibold text-destructive">
|
||||
{{ getAlertSummary() }}
|
||||
@@ -15,7 +15,7 @@
|
||||
</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') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -63,12 +63,14 @@
|
||||
|
||||
// 统计各类型警报数量
|
||||
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 => {
|
||||
if (alert.missionType === MissionType.Spy) {
|
||||
counts.spy++
|
||||
} else if (alert.missionType === MissionType.Attack) {
|
||||
counts.attack++
|
||||
} else if (alert.missionType === MissionType.Recycle) {
|
||||
counts.recycle++
|
||||
} else {
|
||||
counts.other++
|
||||
}
|
||||
@@ -85,6 +87,9 @@
|
||||
if (alertCounts.value.spy > 0) {
|
||||
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) {
|
||||
parts.push(`${alertCounts.value.other} ${t('enemyAlert.missionType.unknown')}`)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<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 flex-shrink-0 animate-pulse" />
|
||||
<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') }}
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 建造电站按钮 -->
|
||||
<Button @click="goToBuildSolarPlant" variant="outline" size="sm" class="flex-shrink-0">
|
||||
<Button @click="goToBuildSolarPlant" variant="outline" size="sm" class="shrink-0">
|
||||
{{ t('energy.buildSolarPlant') }}
|
||||
</Button>
|
||||
</div>
|
||||
119
src/components/notifications/OreDepositWarning.vue
Normal file
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
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>
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="hidden sm:flex items-center gap-3">
|
||||
<!-- 状态指示器 -->
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': status === RelationStatus.Friendly,
|
||||
'bg-red-500': status === RelationStatus.Hostile,
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 好感度 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div class="w-16 h-1.5 bg-muted rounded-full overflow-hidden relative">
|
||||
<div v-if="reputation < 0" class="h-full bg-red-500 absolute right-1/2" :style="{ width: `${Math.abs(reputation) / 2}%` }" />
|
||||
<div v-if="reputation > 0" class="h-full bg-green-500 absolute left-1/2" :style="{ width: `${reputation / 2}%` }" />
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click.stop="handleGiftResources" :title="t('diplomacy.actions.gift')">
|
||||
<Gift class="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -70,7 +70,7 @@
|
||||
<!-- 第一行:状态、名称、展开箭头 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': status === RelationStatus.Friendly,
|
||||
'bg-red-500': status === RelationStatus.Hostile,
|
||||
@@ -90,7 +90,7 @@
|
||||
Lv.{{ npc.difficultyLevel }}
|
||||
</Badge>
|
||||
</div>
|
||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" />
|
||||
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform shrink-0" :class="{ 'rotate-180': isExpanded }" />
|
||||
</div>
|
||||
|
||||
<!-- 第二行:星球数、好感度、操作按钮 -->
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:class="cn('relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]', props.class)"
|
||||
:class="cn('relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,#262626_0%,#000_100%)]', props.class)"
|
||||
@mousemove="handleMouseMove"
|
||||
>
|
||||
<motion.div :style="{ x: springX, y: springY }">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-sm border shadow-xs transition-shadow outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-60 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[60] bg-black/80',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-60 bg-black/80',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-60 w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
||||
containerClass
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
|
||||
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="popover"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<PopoverRoot v-slot="slotProps" data-slot="popover" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from "reka-ui"
|
||||
import { PopoverAnchor } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverAnchor
|
||||
data-slot="popover-anchor"
|
||||
v-bind="props"
|
||||
>
|
||||
<PopoverAnchor data-slot="popover-anchor" v-bind="props">
|
||||
<slot />
|
||||
</PopoverAnchor>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from 'reka-ui'
|
||||
import { PopoverAnchor } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,32 +1,3 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
align: "center",
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
@@ -35,7 +6,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||
props.class,
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -43,3 +14,25 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
align: 'center',
|
||||
sideOffset: 4
|
||||
})
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from "reka-ui"
|
||||
import { PopoverTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger
|
||||
data-slot="popover-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<PopoverTrigger data-slot="popover-trigger" v-bind="props">
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from 'reka-ui'
|
||||
import { PopoverTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as Popover } from "./Popover.vue"
|
||||
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
|
||||
export { default as PopoverContent } from "./PopoverContent.vue"
|
||||
export { default as PopoverTrigger } from "./PopoverTrigger.vue"
|
||||
export { default as Popover } from './Popover.vue'
|
||||
export { default as PopoverAnchor } from './PopoverAnchor.vue'
|
||||
export { default as PopoverContent } from './PopoverContent.vue'
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
props.class
|
||||
@@ -17,7 +17,7 @@
|
||||
:class="
|
||||
cn(
|
||||
'p-1',
|
||||
position === 'popper' && 'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1'
|
||||
position === 'popper' && 'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width) scroll-my-1'
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
|
||||
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import type { TooltipRootEmits, TooltipRootProps } from 'reka-ui'
|
||||
import { TooltipRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<TooltipRootProps>()
|
||||
const emits = defineEmits<TooltipRootEmits>()
|
||||
const props = defineProps<TooltipRootProps>()
|
||||
const emits = defineEmits<TooltipRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TooltipPortal>
|
||||
<TooltipPortal to="#app">
|
||||
<TooltipContent
|
||||
data-slot="tooltip-content"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
@@ -18,22 +18,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
sideOffset: 4
|
||||
})
|
||||
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
sideOffset: 4
|
||||
})
|
||||
|
||||
const emits = defineEmits<TooltipContentEmits>()
|
||||
const emits = defineEmits<TooltipContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TooltipProviderProps } from 'reka-ui'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
import type { TooltipProviderProps } from 'reka-ui'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
|
||||
const props = withDefaults(defineProps<TooltipProviderProps>(), {
|
||||
delayDuration: 0
|
||||
})
|
||||
const props = withDefaults(defineProps<TooltipProviderProps>(), {
|
||||
delayDuration: 0
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TooltipTriggerProps } from 'reka-ui'
|
||||
import { TooltipTrigger } from 'reka-ui'
|
||||
import type { TooltipTriggerProps } from 'reka-ui'
|
||||
import { TooltipTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<TooltipTriggerProps>()
|
||||
const props = defineProps<TooltipTriggerProps>()
|
||||
</script>
|
||||
|
||||
165
src/composables/useQueueAnimation.ts
Normal file
165
src/composables/useQueueAnimation.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 队列添加动画 - 贝塞尔曲线抛物线效果
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface AnimatingItem {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
targetX: number
|
||||
targetY: number
|
||||
progress: number
|
||||
type: 'building' | 'technology' | 'ship' | 'defense'
|
||||
}
|
||||
|
||||
// 全局动画状态
|
||||
const animatingItems = ref<AnimatingItem[]>([])
|
||||
|
||||
// 贝塞尔曲线计算
|
||||
const cubicBezier = (t: number, p0: number, p1: number, p2: number, p3: number): number => {
|
||||
const t2 = t * t
|
||||
const t3 = t2 * t
|
||||
const mt = 1 - t
|
||||
const mt2 = mt * mt
|
||||
const mt3 = mt2 * mt
|
||||
return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3
|
||||
}
|
||||
|
||||
// 获取队列按钮的位置
|
||||
const getQueueButtonPosition = (): { x: number; y: number } => {
|
||||
const queueButton = document.querySelector('[data-tutorial="queue-button"]')
|
||||
if (queueButton) {
|
||||
const rect = queueButton.getBoundingClientRect()
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
}
|
||||
}
|
||||
// 默认右上角位置
|
||||
return { x: window.innerWidth - 50, y: 50 }
|
||||
}
|
||||
|
||||
// 创建动画元素
|
||||
const createAnimationElement = (type: string): HTMLElement => {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'queue-fly-animation'
|
||||
|
||||
// 根据类型设置不同的颜色
|
||||
const colors: Record<string, string> = {
|
||||
building: '#22c55e', // green
|
||||
technology: '#3b82f6', // blue
|
||||
ship: '#a855f7', // purple
|
||||
defense: '#f59e0b' // amber
|
||||
}
|
||||
|
||||
el.style.cssText = `
|
||||
position: fixed;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: ${colors[type] || '#22c55e'};
|
||||
box-shadow: 0 0 10px ${colors[type] || '#22c55e'}, 0 0 20px ${colors[type] || '#22c55e'}80;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transform: translate(-50%, -50%);
|
||||
`
|
||||
|
||||
document.body.appendChild(el)
|
||||
return el
|
||||
}
|
||||
|
||||
// 执行抛物线动画
|
||||
export const triggerQueueAnimation = (event: MouseEvent, type: 'building' | 'technology' | 'ship' | 'defense' = 'building'): void => {
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const target = getQueueButtonPosition()
|
||||
|
||||
const el = createAnimationElement(type)
|
||||
|
||||
// 动画参数
|
||||
const duration = 500 // ms
|
||||
const startTime = performance.now()
|
||||
|
||||
// 计算控制点(向上拱起的抛物线)
|
||||
const midX = (startX + target.x) / 2
|
||||
|
||||
// 控制点:创建一个向上凸起的曲线
|
||||
const cp1x = startX + (midX - startX) * 0.5
|
||||
const cp1y = startY - 80
|
||||
const cp2x = target.x - (target.x - midX) * 0.5
|
||||
const cp2y = target.y - 80
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// 使用缓动函数让动画更自然
|
||||
const easeOutCubic = 1 - Math.pow(1 - progress, 3)
|
||||
|
||||
// 计算当前位置(三次贝塞尔曲线)
|
||||
const x = cubicBezier(easeOutCubic, startX, cp1x, cp2x, target.x)
|
||||
const y = cubicBezier(easeOutCubic, startY, cp1y, cp2y, target.y)
|
||||
|
||||
// 缩放效果:开始时放大,结束时缩小
|
||||
const scale = 1 + Math.sin(progress * Math.PI) * 0.5
|
||||
|
||||
// 透明度:结束时淡出
|
||||
const opacity = progress < 0.8 ? 1 : 1 - (progress - 0.8) / 0.2
|
||||
|
||||
el.style.left = `${x}px`
|
||||
el.style.top = `${y}px`
|
||||
el.style.transform = `translate(-50%, -50%) scale(${scale})`
|
||||
el.style.opacity = `${opacity}`
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
} else {
|
||||
// 动画结束,移除元素
|
||||
el.remove()
|
||||
|
||||
// 触发队列按钮的脉冲效果
|
||||
const queueButton = document.querySelector('[data-tutorial="queue-button"]')
|
||||
if (queueButton) {
|
||||
queueButton.classList.add('queue-pulse')
|
||||
setTimeout(() => {
|
||||
queueButton.classList.remove('queue-pulse')
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 批量动画(用于一次添加多个项目)
|
||||
export const triggerMultipleQueueAnimations = (
|
||||
event: MouseEvent,
|
||||
count: number,
|
||||
type: 'building' | 'technology' | 'ship' | 'defense' = 'building'
|
||||
): void => {
|
||||
const delay = 50 // 每个动画之间的延迟
|
||||
const maxAnimations = Math.min(count, 5) // 最多显示5个动画
|
||||
|
||||
for (let i = 0; i < maxAnimations; i++) {
|
||||
setTimeout(() => {
|
||||
// 添加一些随机偏移让动画更自然
|
||||
const offsetEvent = {
|
||||
...event,
|
||||
clientX: event.clientX + (Math.random() - 0.5) * 20,
|
||||
clientY: event.clientY + (Math.random() - 0.5) * 20
|
||||
} as MouseEvent
|
||||
|
||||
triggerQueueAnimation(offsetEvent, type)
|
||||
}, i * delay)
|
||||
}
|
||||
}
|
||||
|
||||
export const useQueueAnimation = () => {
|
||||
return {
|
||||
animatingItems,
|
||||
triggerQueueAnimation,
|
||||
triggerMultipleQueueAnimations
|
||||
}
|
||||
}
|
||||
1100
src/config/campaignConfig.ts
Normal file
1100
src/config/campaignConfig.ts
Normal file
@@ -0,0 +1,1100 @@
|
||||
/**
|
||||
* 战役配置 - 神秘探索主题
|
||||
* 包含5个章节,共25个任务
|
||||
*/
|
||||
|
||||
import { BuildingType, TechnologyType, ShipType, ObjectiveType } from '@/types/game'
|
||||
import type { CampaignConfig, CampaignChapterConfig, CampaignQuestConfig } from '@/types/game'
|
||||
|
||||
// ==================== 第一章:起源之地 ====================
|
||||
const chapter1Quests: CampaignQuestConfig[] = [
|
||||
{
|
||||
id: 'quest_1_1',
|
||||
chapter: 1,
|
||||
order: 1,
|
||||
titleKey: 'campaign.quests.1_1.title',
|
||||
descriptionKey: 'campaign.quests.1_1.description',
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_1_1_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.1_1.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_1_1_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.1_1.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_1_1_1',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildMetalMine',
|
||||
target: BuildingType.MetalMine,
|
||||
targetSecondary: 2,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_1_2',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildCrystalMine',
|
||||
target: BuildingType.CrystalMine,
|
||||
targetSecondary: 2,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_1_3',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildSolarPlant',
|
||||
target: BuildingType.SolarPlant,
|
||||
targetSecondary: 2,
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 5000, crystal: 2500, deuterium: 0 },
|
||||
darkMatter: 50,
|
||||
points: 100
|
||||
},
|
||||
mapPosition: { x: 50, y: 20 }
|
||||
},
|
||||
{
|
||||
id: 'quest_1_2',
|
||||
chapter: 1,
|
||||
order: 2,
|
||||
titleKey: 'campaign.quests.1_2.title',
|
||||
descriptionKey: 'campaign.quests.1_2.description',
|
||||
requiredQuestIds: ['quest_1_1'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_1_2_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.1_2.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_1_2_1',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildResearchLab',
|
||||
target: BuildingType.ResearchLab,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_2_2',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchEnergy',
|
||||
target: TechnologyType.EnergyTechnology,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 3000, crystal: 3000, deuterium: 1000 },
|
||||
darkMatter: 75,
|
||||
points: 150
|
||||
},
|
||||
mapPosition: { x: 50, y: 35 }
|
||||
},
|
||||
{
|
||||
id: 'quest_1_3',
|
||||
chapter: 1,
|
||||
order: 3,
|
||||
titleKey: 'campaign.quests.1_3.title',
|
||||
descriptionKey: 'campaign.quests.1_3.description',
|
||||
requiredQuestIds: ['quest_1_2'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_1_3_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.1_3.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_1_3_1',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildShipyard',
|
||||
target: BuildingType.Shipyard,
|
||||
targetSecondary: 2,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_3_2',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchCombustion',
|
||||
target: TechnologyType.CombustionDrive,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_3_3',
|
||||
type: ObjectiveType.ProduceShips,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildLightFighters',
|
||||
target: ShipType.LightFighter,
|
||||
required: 5
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 5000, crystal: 2000, deuterium: 500 },
|
||||
ships: { [ShipType.SmallCargo]: 2 },
|
||||
darkMatter: 100,
|
||||
points: 200
|
||||
},
|
||||
mapPosition: { x: 50, y: 50 }
|
||||
},
|
||||
{
|
||||
id: 'quest_1_4',
|
||||
chapter: 1,
|
||||
order: 4,
|
||||
titleKey: 'campaign.quests.1_4.title',
|
||||
descriptionKey: 'campaign.quests.1_4.description',
|
||||
requiredQuestIds: ['quest_1_3'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_1_4_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.1_4.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_1_4_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.1_4.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_1_4_1',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchEspionage',
|
||||
target: TechnologyType.EspionageTechnology,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_1_4_2',
|
||||
type: ObjectiveType.ProduceShips,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildSpyProbes',
|
||||
target: ShipType.EspionageProbe,
|
||||
required: 3
|
||||
},
|
||||
{
|
||||
id: 'obj_1_4_3',
|
||||
type: ObjectiveType.SpyTarget,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.spyAnyNPC',
|
||||
target: 'any',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 4000, crystal: 4000, deuterium: 1000 },
|
||||
darkMatter: 150,
|
||||
points: 250
|
||||
},
|
||||
mapPosition: { x: 50, y: 65 }
|
||||
},
|
||||
{
|
||||
id: 'quest_1_5',
|
||||
chapter: 1,
|
||||
order: 5,
|
||||
titleKey: 'campaign.quests.1_5.title',
|
||||
descriptionKey: 'campaign.quests.1_5.description',
|
||||
requiredQuestIds: ['quest_1_4'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_1_5_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.1_5.prologue_1'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_1_5_epilogue_1',
|
||||
speaker: 'npc',
|
||||
speakerNameKey: 'campaign.speakers.neighborNPC',
|
||||
textKey: 'campaign.dialogues.1_5.epilogue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_1_5_epilogue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.1_5.epilogue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_1_5_1',
|
||||
type: ObjectiveType.SendGift,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.sendGiftToNPC',
|
||||
target: 'any',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 10000, crystal: 5000, deuterium: 2000 },
|
||||
darkMatter: 200,
|
||||
points: 500
|
||||
},
|
||||
mapPosition: { x: 50, y: 80 }
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 第二章:星际探索 ====================
|
||||
const chapter2Quests: CampaignQuestConfig[] = [
|
||||
{
|
||||
id: 'quest_2_1',
|
||||
chapter: 2,
|
||||
order: 1,
|
||||
titleKey: 'campaign.quests.2_1.title',
|
||||
descriptionKey: 'campaign.quests.2_1.description',
|
||||
requiredQuestIds: ['quest_1_5'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_2_1_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.2_1.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_2_1_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.2_1.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_2_1_1',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchAstrophysics',
|
||||
target: TechnologyType.Astrophysics,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_2_1_2',
|
||||
type: ObjectiveType.ProduceShips,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildColonyShip',
|
||||
target: ShipType.ColonyShip,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_2_1_3',
|
||||
type: ObjectiveType.Colonize,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.colonizeNewPlanet',
|
||||
target: 'any',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 15000, crystal: 10000, deuterium: 5000 },
|
||||
darkMatter: 300,
|
||||
points: 600
|
||||
},
|
||||
mapPosition: { x: 30, y: 20 }
|
||||
},
|
||||
{
|
||||
id: 'quest_2_2',
|
||||
chapter: 2,
|
||||
order: 2,
|
||||
titleKey: 'campaign.quests.2_2.title',
|
||||
descriptionKey: 'campaign.quests.2_2.description',
|
||||
requiredQuestIds: ['quest_2_1'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_2_2_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.2_2.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_2_2_1',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchAstrophysicsHigher',
|
||||
target: TechnologyType.Astrophysics,
|
||||
targetSecondary: 3,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_2_2_2',
|
||||
type: ObjectiveType.Expedition,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.completeExpedition',
|
||||
target: 'any',
|
||||
required: 3
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 20000, crystal: 15000, deuterium: 8000 },
|
||||
darkMatter: 500,
|
||||
points: 800
|
||||
},
|
||||
mapPosition: { x: 30, y: 35 }
|
||||
},
|
||||
{
|
||||
id: 'quest_2_3',
|
||||
chapter: 2,
|
||||
order: 3,
|
||||
titleKey: 'campaign.quests.2_3.title',
|
||||
descriptionKey: 'campaign.quests.2_3.description',
|
||||
requiredQuestIds: ['quest_2_2'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_2_3_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.2_3.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_2_3_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.2_3.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_2_3_1',
|
||||
type: ObjectiveType.Expedition,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.expeditionDeepSpace',
|
||||
target: 'deepSpace',
|
||||
required: 2
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 25000, crystal: 20000, deuterium: 10000 },
|
||||
darkMatter: 750,
|
||||
points: 1000
|
||||
},
|
||||
mapPosition: { x: 30, y: 50 }
|
||||
},
|
||||
{
|
||||
id: 'quest_2_4',
|
||||
chapter: 2,
|
||||
order: 4,
|
||||
titleKey: 'campaign.quests.2_4.title',
|
||||
descriptionKey: 'campaign.quests.2_4.description',
|
||||
requiredQuestIds: ['quest_2_3'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_2_4_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.2_4.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_2_4_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.2_4.prologue_2',
|
||||
choices: [
|
||||
{
|
||||
textKey: 'campaign.dialogues.2_4.choice_1',
|
||||
effect: 'reputation_up'
|
||||
},
|
||||
{
|
||||
textKey: 'campaign.dialogues.2_4.choice_2',
|
||||
effect: 'unlock_branch'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_2_4_1',
|
||||
type: ObjectiveType.Expedition,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.expeditionUncharted',
|
||||
target: 'unchartedSpace',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 30000, crystal: 25000, deuterium: 15000 },
|
||||
darkMatter: 1000,
|
||||
points: 1500
|
||||
},
|
||||
mapPosition: { x: 30, y: 65 }
|
||||
},
|
||||
{
|
||||
id: 'quest_2_5',
|
||||
chapter: 2,
|
||||
order: 5,
|
||||
titleKey: 'campaign.quests.2_5.title',
|
||||
descriptionKey: 'campaign.quests.2_5.description',
|
||||
requiredQuestIds: ['quest_2_4'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_2_5_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.2_5.prologue_1'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_2_5_epilogue_1',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.2_5.epilogue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_2_5_1',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchImpulse',
|
||||
target: TechnologyType.ImpulseDrive,
|
||||
targetSecondary: 3,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_2_5_2',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchLaser',
|
||||
target: TechnologyType.LaserTechnology,
|
||||
targetSecondary: 5,
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 40000, crystal: 30000, deuterium: 20000 },
|
||||
darkMatter: 1500,
|
||||
points: 2000
|
||||
},
|
||||
mapPosition: { x: 30, y: 80 }
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 第三章:银河外交 ====================
|
||||
const chapter3Quests: CampaignQuestConfig[] = [
|
||||
{
|
||||
id: 'quest_3_1',
|
||||
chapter: 3,
|
||||
order: 1,
|
||||
titleKey: 'campaign.quests.3_1.title',
|
||||
descriptionKey: 'campaign.quests.3_1.description',
|
||||
requiredQuestIds: ['quest_2_5'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_3_1_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.3_1.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_3_1_1',
|
||||
type: ObjectiveType.SendGift,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.sendMultipleGifts',
|
||||
target: 'any',
|
||||
required: 3
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 25000, crystal: 20000, deuterium: 10000 },
|
||||
darkMatter: 500,
|
||||
points: 1000
|
||||
},
|
||||
mapPosition: { x: 70, y: 20 }
|
||||
},
|
||||
{
|
||||
id: 'quest_3_2',
|
||||
chapter: 3,
|
||||
order: 2,
|
||||
titleKey: 'campaign.quests.3_2.title',
|
||||
descriptionKey: 'campaign.quests.3_2.description',
|
||||
requiredQuestIds: ['quest_3_1'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_3_2_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.3_2.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_3_2_1',
|
||||
type: ObjectiveType.ReachRelation,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.reachFriendlyRelation',
|
||||
target: 'friendly',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 30000, crystal: 25000, deuterium: 15000 },
|
||||
darkMatter: 750,
|
||||
points: 1200
|
||||
},
|
||||
mapPosition: { x: 70, y: 35 }
|
||||
},
|
||||
{
|
||||
id: 'quest_3_3',
|
||||
chapter: 3,
|
||||
order: 3,
|
||||
titleKey: 'campaign.quests.3_3.title',
|
||||
descriptionKey: 'campaign.quests.3_3.description',
|
||||
requiredQuestIds: ['quest_3_2'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_3_3_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.3_3.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_3_3_prologue_2',
|
||||
speaker: 'npc',
|
||||
speakerNameKey: 'campaign.speakers.allyNPC',
|
||||
textKey: 'campaign.dialogues.3_3.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_3_3_1',
|
||||
type: ObjectiveType.SpyTarget,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.spyHostileNPC',
|
||||
target: 'hostile',
|
||||
required: 2
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 35000, crystal: 30000, deuterium: 18000 },
|
||||
darkMatter: 1000,
|
||||
points: 1500
|
||||
},
|
||||
mapPosition: { x: 70, y: 50 }
|
||||
},
|
||||
{
|
||||
id: 'quest_3_4',
|
||||
chapter: 3,
|
||||
order: 4,
|
||||
titleKey: 'campaign.quests.3_4.title',
|
||||
descriptionKey: 'campaign.quests.3_4.description',
|
||||
requiredQuestIds: ['quest_3_3'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_3_4_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.3_4.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_3_4_1',
|
||||
type: ObjectiveType.FormAlliance,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.formAlliance',
|
||||
target: 'any',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 40000, crystal: 35000, deuterium: 20000 },
|
||||
darkMatter: 1250,
|
||||
points: 1800
|
||||
},
|
||||
mapPosition: { x: 70, y: 65 }
|
||||
},
|
||||
{
|
||||
id: 'quest_3_5',
|
||||
chapter: 3,
|
||||
order: 5,
|
||||
titleKey: 'campaign.quests.3_5.title',
|
||||
descriptionKey: 'campaign.quests.3_5.description',
|
||||
requiredQuestIds: ['quest_3_4'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_3_5_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.3_5.prologue_1'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_3_5_epilogue_1',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.3_5.epilogue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_3_5_1',
|
||||
type: ObjectiveType.BuildBuilding,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildMissileSilo',
|
||||
target: BuildingType.MissileSilo,
|
||||
targetSecondary: 2,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_3_5_2',
|
||||
type: ObjectiveType.ProduceShips,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildCruisers',
|
||||
target: ShipType.Cruiser,
|
||||
required: 10
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 50000, crystal: 40000, deuterium: 25000 },
|
||||
darkMatter: 2000,
|
||||
points: 2500
|
||||
},
|
||||
mapPosition: { x: 70, y: 80 }
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 第四章:暗影降临 ====================
|
||||
const chapter4Quests: CampaignQuestConfig[] = [
|
||||
{
|
||||
id: 'quest_4_1',
|
||||
chapter: 4,
|
||||
order: 1,
|
||||
titleKey: 'campaign.quests.4_1.title',
|
||||
descriptionKey: 'campaign.quests.4_1.description',
|
||||
requiredQuestIds: ['quest_3_5'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_4_1_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.4_1.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_4_1_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.shadowVoice',
|
||||
textKey: 'campaign.dialogues.4_1.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_4_1_1',
|
||||
type: ObjectiveType.WinBattles,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.defendAgainstAttack',
|
||||
target: 'defense',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 40000, crystal: 35000, deuterium: 20000 },
|
||||
darkMatter: 1000,
|
||||
points: 1500
|
||||
},
|
||||
mapPosition: { x: 50, y: 20 }
|
||||
},
|
||||
{
|
||||
id: 'quest_4_2',
|
||||
chapter: 4,
|
||||
order: 2,
|
||||
titleKey: 'campaign.quests.4_2.title',
|
||||
descriptionKey: 'campaign.quests.4_2.description',
|
||||
requiredQuestIds: ['quest_4_1'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_4_2_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.4_2.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_4_2_1',
|
||||
type: ObjectiveType.SpyTarget,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.spyEnemyPlanets',
|
||||
target: 'hostile',
|
||||
required: 5
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 45000, crystal: 40000, deuterium: 25000 },
|
||||
darkMatter: 1250,
|
||||
points: 1800
|
||||
},
|
||||
mapPosition: { x: 50, y: 35 }
|
||||
},
|
||||
{
|
||||
id: 'quest_4_3',
|
||||
chapter: 4,
|
||||
order: 3,
|
||||
titleKey: 'campaign.quests.4_3.title',
|
||||
descriptionKey: 'campaign.quests.4_3.description',
|
||||
requiredQuestIds: ['quest_4_2'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_4_3_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.4_3.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_4_3_1',
|
||||
type: ObjectiveType.WinBattles,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.winAttackBattles',
|
||||
target: 'attack',
|
||||
required: 3
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 50000, crystal: 45000, deuterium: 30000 },
|
||||
darkMatter: 1500,
|
||||
points: 2000
|
||||
},
|
||||
mapPosition: { x: 50, y: 50 }
|
||||
},
|
||||
{
|
||||
id: 'quest_4_4',
|
||||
chapter: 4,
|
||||
order: 4,
|
||||
titleKey: 'campaign.quests.4_4.title',
|
||||
descriptionKey: 'campaign.quests.4_4.description',
|
||||
requiredQuestIds: ['quest_4_3'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_4_4_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.4_4.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_4_4_1',
|
||||
type: ObjectiveType.RecycleDebris,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.recycleDebris',
|
||||
target: 'any',
|
||||
required: 5
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 55000, crystal: 50000, deuterium: 35000 },
|
||||
darkMatter: 1750,
|
||||
points: 2200
|
||||
},
|
||||
mapPosition: { x: 50, y: 65 }
|
||||
},
|
||||
{
|
||||
id: 'quest_4_5',
|
||||
chapter: 4,
|
||||
order: 5,
|
||||
titleKey: 'campaign.quests.4_5.title',
|
||||
descriptionKey: 'campaign.quests.4_5.description',
|
||||
requiredQuestIds: ['quest_4_4'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_4_5_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.4_5.prologue_1'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_4_5_epilogue_1',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.4_5.epilogue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_4_5_1',
|
||||
type: ObjectiveType.ProduceShips,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.buildBattleships',
|
||||
target: ShipType.Battleship,
|
||||
required: 20
|
||||
},
|
||||
{
|
||||
id: 'obj_4_5_2',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchHyperspace',
|
||||
target: TechnologyType.HyperspaceDrive,
|
||||
targetSecondary: 3,
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 70000, crystal: 60000, deuterium: 40000 },
|
||||
darkMatter: 2500,
|
||||
points: 3000
|
||||
},
|
||||
mapPosition: { x: 50, y: 80 }
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 第五章:古代秘密 ====================
|
||||
const chapter5Quests: CampaignQuestConfig[] = [
|
||||
{
|
||||
id: 'quest_5_1',
|
||||
chapter: 5,
|
||||
order: 1,
|
||||
titleKey: 'campaign.quests.5_1.title',
|
||||
descriptionKey: 'campaign.quests.5_1.description',
|
||||
requiredQuestIds: ['quest_4_5'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_5_1_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_1.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_5_1_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.5_1.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_5_1_1',
|
||||
type: ObjectiveType.Expedition,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.expeditionDangerous',
|
||||
target: 'dangerousNebula',
|
||||
required: 3
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 80000, crystal: 70000, deuterium: 50000 },
|
||||
darkMatter: 3000,
|
||||
points: 4000
|
||||
},
|
||||
mapPosition: { x: 50, y: 15 }
|
||||
},
|
||||
{
|
||||
id: 'quest_5_2',
|
||||
chapter: 5,
|
||||
order: 2,
|
||||
titleKey: 'campaign.quests.5_2.title',
|
||||
descriptionKey: 'campaign.quests.5_2.description',
|
||||
requiredQuestIds: ['quest_5_1'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_5_2_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_2.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_5_2_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientGuardian',
|
||||
textKey: 'campaign.dialogues.5_2.prologue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_5_2_1',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchIntergalactic',
|
||||
target: TechnologyType.ComputerTechnology,
|
||||
targetSecondary: 10,
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
id: 'obj_5_2_2',
|
||||
type: ObjectiveType.ResearchTech,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.researchGraviton',
|
||||
target: TechnologyType.GravitonTechnology,
|
||||
targetSecondary: 1,
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 100000, crystal: 80000, deuterium: 60000 },
|
||||
darkMatter: 5000,
|
||||
points: 5000
|
||||
},
|
||||
mapPosition: { x: 50, y: 32 }
|
||||
},
|
||||
{
|
||||
id: 'quest_5_3',
|
||||
chapter: 5,
|
||||
order: 3,
|
||||
titleKey: 'campaign.quests.5_3.title',
|
||||
descriptionKey: 'campaign.quests.5_3.description',
|
||||
requiredQuestIds: ['quest_5_2'],
|
||||
isBoss: true,
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_5_3_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_3.prologue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_5_3_prologue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientGuardian',
|
||||
textKey: 'campaign.dialogues.5_3.prologue_2'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_5_3_epilogue_1',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientGuardian',
|
||||
textKey: 'campaign.dialogues.5_3.epilogue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_5_3_1',
|
||||
type: ObjectiveType.DefeatNPC,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.defeatBoss',
|
||||
target: 'boss',
|
||||
required: 1
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 150000, crystal: 120000, deuterium: 80000 },
|
||||
ships: { [ShipType.Deathstar]: 1 },
|
||||
darkMatter: 10000,
|
||||
points: 10000
|
||||
},
|
||||
mapPosition: { x: 50, y: 50 }
|
||||
},
|
||||
{
|
||||
id: 'quest_5_4',
|
||||
chapter: 5,
|
||||
order: 4,
|
||||
titleKey: 'campaign.quests.5_4.title',
|
||||
descriptionKey: 'campaign.quests.5_4.description',
|
||||
requiredQuestIds: ['quest_5_3'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_5_4_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_4.prologue_1'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_5_4_1',
|
||||
type: ObjectiveType.Colonize,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.colonizeMultiple',
|
||||
target: 'any',
|
||||
required: 5
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 200000, crystal: 150000, deuterium: 100000 },
|
||||
darkMatter: 8000,
|
||||
points: 8000
|
||||
},
|
||||
mapPosition: { x: 50, y: 68 }
|
||||
},
|
||||
{
|
||||
id: 'quest_5_5',
|
||||
chapter: 5,
|
||||
order: 5,
|
||||
titleKey: 'campaign.quests.5_5.title',
|
||||
descriptionKey: 'campaign.quests.5_5.description',
|
||||
requiredQuestIds: ['quest_5_4'],
|
||||
prologueDialogues: [
|
||||
{
|
||||
id: 'quest_5_5_prologue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_5.prologue_1'
|
||||
}
|
||||
],
|
||||
epilogueDialogues: [
|
||||
{
|
||||
id: 'quest_5_5_epilogue_1',
|
||||
speaker: 'narrator',
|
||||
textKey: 'campaign.dialogues.5_5.epilogue_1'
|
||||
},
|
||||
{
|
||||
id: 'quest_5_5_epilogue_2',
|
||||
speaker: 'mysterious',
|
||||
speakerNameKey: 'campaign.speakers.ancientVoice',
|
||||
textKey: 'campaign.dialogues.5_5.epilogue_2'
|
||||
}
|
||||
],
|
||||
objectives: [
|
||||
{
|
||||
id: 'obj_5_5_1',
|
||||
type: ObjectiveType.AccumulateResources,
|
||||
descriptionKey: 'campaign.objectiveDescriptions.accumulateWealth',
|
||||
target: 'total',
|
||||
required: 1000000
|
||||
}
|
||||
],
|
||||
rewards: {
|
||||
resources: { metal: 500000, crystal: 400000, deuterium: 250000 },
|
||||
darkMatter: 20000,
|
||||
points: 20000
|
||||
},
|
||||
mapPosition: { x: 50, y: 85 }
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 章节配置 ====================
|
||||
const chapters: CampaignChapterConfig[] = [
|
||||
{
|
||||
id: 'chapter_1',
|
||||
number: 1,
|
||||
titleKey: 'campaign.chapters.1.title',
|
||||
descriptionKey: 'campaign.chapters.1.description',
|
||||
backgroundStoryKey: 'campaign.chapters.1.backgroundStory',
|
||||
quests: chapter1Quests
|
||||
},
|
||||
{
|
||||
id: 'chapter_2',
|
||||
number: 2,
|
||||
titleKey: 'campaign.chapters.2.title',
|
||||
descriptionKey: 'campaign.chapters.2.description',
|
||||
backgroundStoryKey: 'campaign.chapters.2.backgroundStory',
|
||||
quests: chapter2Quests
|
||||
},
|
||||
{
|
||||
id: 'chapter_3',
|
||||
number: 3,
|
||||
titleKey: 'campaign.chapters.3.title',
|
||||
descriptionKey: 'campaign.chapters.3.description',
|
||||
backgroundStoryKey: 'campaign.chapters.3.backgroundStory',
|
||||
quests: chapter3Quests
|
||||
},
|
||||
{
|
||||
id: 'chapter_4',
|
||||
number: 4,
|
||||
titleKey: 'campaign.chapters.4.title',
|
||||
descriptionKey: 'campaign.chapters.4.description',
|
||||
backgroundStoryKey: 'campaign.chapters.4.backgroundStory',
|
||||
quests: chapter4Quests
|
||||
},
|
||||
{
|
||||
id: 'chapter_5',
|
||||
number: 5,
|
||||
titleKey: 'campaign.chapters.5.title',
|
||||
descriptionKey: 'campaign.chapters.5.description',
|
||||
backgroundStoryKey: 'campaign.chapters.5.backgroundStory',
|
||||
quests: chapter5Quests
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 主战役配置 ====================
|
||||
export const MAIN_CAMPAIGN: CampaignConfig = {
|
||||
id: 'main_campaign',
|
||||
nameKey: 'campaign.name',
|
||||
descriptionKey: 'campaign.description',
|
||||
chapters
|
||||
}
|
||||
|
||||
// 获取所有任务的扁平列表
|
||||
export const getAllQuests = (): CampaignQuestConfig[] => {
|
||||
return MAIN_CAMPAIGN.chapters.flatMap(chapter => chapter.quests)
|
||||
}
|
||||
|
||||
// 根据ID获取任务配置
|
||||
export const getQuestById = (questId: string): CampaignQuestConfig | undefined => {
|
||||
return getAllQuests().find(quest => quest.id === questId)
|
||||
}
|
||||
|
||||
// 根据章节获取任务列表
|
||||
export const getQuestsByChapter = (chapterNumber: number): CampaignQuestConfig[] => {
|
||||
const chapter = MAIN_CAMPAIGN.chapters.find(c => c.number === chapterNumber)
|
||||
return chapter?.quests || []
|
||||
}
|
||||
|
||||
// 获取章节配置
|
||||
export const getChapterById = (chapterId: string): CampaignChapterConfig | undefined => {
|
||||
return MAIN_CAMPAIGN.chapters.find(chapter => chapter.id === chapterId)
|
||||
}
|
||||
|
||||
// 获取总任务数
|
||||
export const getTotalQuestCount = (): number => {
|
||||
return getAllQuests().length
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
import type { BuildingConfig, TechnologyConfig, ShipConfig, DefenseConfig, OfficerConfig } from '@/types/game'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType, ExpeditionZone } from '@/types/game'
|
||||
import type { BuildingConfig, TechnologyConfig, ShipConfig, DefenseConfig, OfficerConfig, ExpeditionZoneConfig } from '@/types/game'
|
||||
|
||||
// 建筑配置数据
|
||||
export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
||||
@@ -835,7 +835,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
||||
[ShipType.SolarSatellite]: {
|
||||
id: ShipType.SolarSatellite,
|
||||
name: '太阳能卫星',
|
||||
description: '提供额外能源,每个产生50点能量',
|
||||
description: '提供额外能源,产能受星球温度影响',
|
||||
cost: { metal: 0, crystal: 2000, deuterium: 500, darkMatter: 0, energy: 0 },
|
||||
buildTime: 10,
|
||||
cargoCapacity: 0,
|
||||
@@ -1199,3 +1199,100 @@ export const DIPLOMATIC_CONFIG = {
|
||||
REJECTION_REPUTATION_PENALTY: -5 // 拒绝礼物导致的好感度降低
|
||||
}
|
||||
}
|
||||
|
||||
// 矿脉储量配置
|
||||
export const ORE_DEPOSIT_CONFIG = {
|
||||
// 基础储量(单位:资源量)
|
||||
BASE_DEPOSITS: {
|
||||
metal: 50_000_000, // 5000万金属
|
||||
crystal: 30_000_000, // 3000万晶体
|
||||
deuterium: 15_000_000 // 1500万重氢
|
||||
},
|
||||
// 位置系数(位置1-15),影响资源分布
|
||||
// 内圈(1-4)晶体多,中圈(5-10)均衡,外圈(11-15)重氢多
|
||||
POSITION_MULTIPLIERS: {
|
||||
metal: [0.8, 0.85, 0.9, 0.95, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.95, 0.9, 0.85, 0.8, 0.75],
|
||||
crystal: [1.3, 1.25, 1.2, 1.1, 1.0, 1.0, 1.0, 1.0, 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7],
|
||||
deuterium: [0.5, 0.55, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7]
|
||||
},
|
||||
// 银河系系数(每个银河系增加5%储量)
|
||||
GALAXY_MULTIPLIER: 0.05,
|
||||
// 随机浮动范围(±20%)
|
||||
RANDOM_VARIANCE: 0.2,
|
||||
// 警告阈值(低于10%时显示警告)
|
||||
WARNING_THRESHOLD: 0.1,
|
||||
// 产量衰减开始阈值(低于5%时产量开始衰减)
|
||||
DECAY_START_THRESHOLD: 0.05
|
||||
}
|
||||
|
||||
// 探险区域配置
|
||||
export const EXPEDITION_ZONES: Record<ExpeditionZone, ExpeditionZoneConfig> = {
|
||||
[ExpeditionZone.NearSpace]: {
|
||||
id: ExpeditionZone.NearSpace,
|
||||
requiredTechLevel: 0, // 无需求
|
||||
flightTimeMultiplier: 1.0,
|
||||
resourceMultiplier: 1.0,
|
||||
darkMatterMultiplier: 1.0,
|
||||
fleetFindMultiplier: 1.0,
|
||||
dangerMultiplier: 0.5, // 低危险
|
||||
probabilities: {
|
||||
resources: 35, // 高概率发现资源
|
||||
darkMatter: 8,
|
||||
fleet: 10,
|
||||
pirates: 7, // 低海盗
|
||||
aliens: 5, // 低外星人
|
||||
nothing: 35 // 高概率什么都没发现
|
||||
}
|
||||
},
|
||||
[ExpeditionZone.DeepSpace]: {
|
||||
id: ExpeditionZone.DeepSpace,
|
||||
requiredTechLevel: 4, // 需要天体物理学4级
|
||||
flightTimeMultiplier: 1.5,
|
||||
resourceMultiplier: 1.5,
|
||||
darkMatterMultiplier: 1.5,
|
||||
fleetFindMultiplier: 1.5,
|
||||
dangerMultiplier: 1.0, // 标准危险
|
||||
probabilities: {
|
||||
resources: 30,
|
||||
darkMatter: 10,
|
||||
fleet: 15,
|
||||
pirates: 15,
|
||||
aliens: 10,
|
||||
nothing: 20
|
||||
}
|
||||
},
|
||||
[ExpeditionZone.UnchartedSpace]: {
|
||||
id: ExpeditionZone.UnchartedSpace,
|
||||
requiredTechLevel: 8, // 需要天体物理学8级
|
||||
flightTimeMultiplier: 2.0,
|
||||
resourceMultiplier: 2.5,
|
||||
darkMatterMultiplier: 2.0,
|
||||
fleetFindMultiplier: 2.0,
|
||||
dangerMultiplier: 1.5, // 高危险
|
||||
probabilities: {
|
||||
resources: 25,
|
||||
darkMatter: 12,
|
||||
fleet: 18,
|
||||
pirates: 18,
|
||||
aliens: 15,
|
||||
nothing: 12
|
||||
}
|
||||
},
|
||||
[ExpeditionZone.DangerousNebula]: {
|
||||
id: ExpeditionZone.DangerousNebula,
|
||||
requiredTechLevel: 12, // 需要天体物理学12级
|
||||
flightTimeMultiplier: 3.0,
|
||||
resourceMultiplier: 4.0,
|
||||
darkMatterMultiplier: 3.0,
|
||||
fleetFindMultiplier: 3.0,
|
||||
dangerMultiplier: 2.5, // 极高危险
|
||||
probabilities: {
|
||||
resources: 20,
|
||||
darkMatter: 15,
|
||||
fleet: 20,
|
||||
pirates: 20,
|
||||
aliens: 20,
|
||||
nothing: 5 // 很少什么都没发现
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,13 @@ export default {
|
||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||
current: 'Aktuell',
|
||||
level: 'Stufe',
|
||||
to: 'bis',
|
||||
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.',
|
||||
view: 'Anzeigen',
|
||||
viewDetails: 'Details anzeigen',
|
||||
exitConfirmTitle: 'Spiel beenden',
|
||||
exitConfirmMessage: 'Möchten Sie das Spiel wirklich beenden? Ihr Fortschritt wird automatisch gespeichert.'
|
||||
exitConfirmMessage: 'Möchten Sie das Spiel wirklich beenden? Ihr Fortschritt wird automatisch gespeichert.',
|
||||
points: 'Punkte'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
galaxy: 'Galaxie',
|
||||
diplomacy: 'Diplomatie',
|
||||
achievements: 'Erfolge',
|
||||
ranking: 'Rangliste',
|
||||
messages: 'Nachrichten',
|
||||
settings: 'Einstellungen',
|
||||
gm: 'GM'
|
||||
@@ -110,7 +114,8 @@ export default {
|
||||
perHour: 'Stunde',
|
||||
perMinute: 'Min',
|
||||
hour: 'Stunde',
|
||||
noEnergy: 'Keine Energie'
|
||||
noEnergy: 'Keine Energie',
|
||||
temperatureBonus: 'Temperaturbonus'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: 'Energiedefizit! Ressourcenproduktion gestoppt!',
|
||||
@@ -120,6 +125,12 @@ export default {
|
||||
deficitDetail: 'Energiedefizit: {deficit}, bauen Sie mehr Kraftwerke',
|
||||
buildSolarPlant: 'Kraftwerk bauen'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: 'Erzvorkommen gehen zur Neige!',
|
||||
depletedWarning: 'Erzvorkommen erschöpft!',
|
||||
depletedResources: 'Erschöpft: {resources}',
|
||||
lowResources: 'Fast erschöpft: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: 'Planet',
|
||||
moon: 'Mond',
|
||||
@@ -186,12 +197,20 @@ export default {
|
||||
researchSpeedBonus: 'Forschungsgeschwindigkeitsbonus',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
missileCapacity: 'Missile Capacity',
|
||||
|
||||
// Erzvorkommen
|
||||
oreDeposit: 'Erzvorkommen',
|
||||
remainingDeposit: 'Verbleibend',
|
||||
depletionTime: 'Erschöpfung',
|
||||
depositDepleted: 'Erschöpft',
|
||||
depositWarning: 'Warnung: Erzvorkommen sind fast erschöpft (unter 10%)!',
|
||||
depositDepletedMessage: 'Erzvorkommen sind erschöpft. Produktion gestoppt.'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Fördert Metallressourcen',
|
||||
crystalMine: 'Fördert Kristallressourcen',
|
||||
deuteriumSynthesizer: 'Synthesiert Deuteriumressourcen',
|
||||
deuteriumSynthesizer: 'Synthesiert Deuterium (höhere Produktion bei niedrigen Temperaturen)',
|
||||
solarPlant: 'Liefert Energie',
|
||||
fusionReactor: 'Nutzt Deuterium zur Erzeugung großer Energiemengen',
|
||||
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
|
||||
@@ -241,7 +260,7 @@ export default {
|
||||
colonyShip: 'Zur Kolonisierung neuer Planeten',
|
||||
recycler: 'Sammelt Trümmerfeld-Ressourcen',
|
||||
espionageProbe: 'Späht feindliche Planeten aus',
|
||||
solarSatellite: 'Liefert zusätzliche Energie, erzeugt 50 Energie pro Satellit',
|
||||
solarSatellite: 'Liefert zusätzliche Energie, Leistung abhängig von Planetentemperatur (höher bei Hitze)',
|
||||
darkMatterHarvester: 'Spezielles Schiff zum Ernten von Dunkler Materie',
|
||||
deathstar: 'Ultimative Waffe, die ganze Planeten zerstören kann'
|
||||
},
|
||||
@@ -366,8 +385,16 @@ export default {
|
||||
buildings: 'Gebäude',
|
||||
research: 'Forschung',
|
||||
ships: 'Schiffe',
|
||||
defense: 'Verteidigung'
|
||||
}
|
||||
defense: 'Verteidigung',
|
||||
waiting: 'Warten'
|
||||
},
|
||||
waitingEmpty: 'Keine wartenden Aufgaben',
|
||||
addToWaiting: 'Zur Warteschlange hinzufügen',
|
||||
remove: 'Entfernen',
|
||||
resourcesReady: 'Bereit',
|
||||
waitingResources: 'Warten',
|
||||
waitingQueueFull: 'Warteschlange ist voll',
|
||||
movedToQueue: 'Aufgabe in Warteschlange verschoben'
|
||||
},
|
||||
overview: {
|
||||
title: 'Planetenübersicht',
|
||||
@@ -380,7 +407,10 @@ export default {
|
||||
consumptionSourcesDesc: 'Energieverbrauchsdetails für Gebäude',
|
||||
totalProduction: 'Gesamtproduktion',
|
||||
totalConsumption: 'Gesamtverbrauch',
|
||||
noConsumption: 'Kein Energieverbrauch'
|
||||
noConsumption: 'Kein Energieverbrauch',
|
||||
tabOverview: 'Übersicht',
|
||||
tabProduction: 'Produktionsdetails',
|
||||
tabConsumption: 'Verbrauchsdetails'
|
||||
},
|
||||
buildingsView: {
|
||||
title: 'Gebäude',
|
||||
@@ -514,6 +544,29 @@ export default {
|
||||
spy: 'Spionage',
|
||||
deploy: 'Stationieren',
|
||||
expedition: 'Expedition',
|
||||
expeditionZone: 'Expeditionszone',
|
||||
expeditionZoneDesc: 'Wähle das Zielgebiet. Verschiedene Zonen haben unterschiedliche Risiken und Belohnungen',
|
||||
requiresAstro: 'Benötigt Astrophysik Stufe {level}',
|
||||
reward: 'Belohnung',
|
||||
danger: 'Gefahr',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: 'Naher Weltraum',
|
||||
desc: 'Sicherer naher Weltraum, geringes Risiko aber weniger Belohnungen'
|
||||
},
|
||||
deepSpace: {
|
||||
name: 'Tiefer Weltraum',
|
||||
desc: 'Weit von Sternen entfernt, mehr Ressourcen können gefunden werden'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: 'Unerforschter Raum',
|
||||
desc: 'Unerforschtes Gebiet, hohes Risiko hohe Belohnung'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: 'Gefährlicher Nebel',
|
||||
desc: 'Nebel voller unbekannter Gefahren, enthält aber extrem reiche Schätze'
|
||||
}
|
||||
},
|
||||
recycle: 'Recyceln',
|
||||
transportResources: 'Ressourcen transportieren',
|
||||
totalCargoCapacity: 'Gesamtladekapazität',
|
||||
@@ -568,7 +621,24 @@ export default {
|
||||
presetName: 'Vorlagenname',
|
||||
presetNamePlaceholder: 'Vorlagennamen eingeben',
|
||||
deletePresetTitle: 'Vorlage löschen',
|
||||
deletePresetMessage: 'Vorlage "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'
|
||||
deletePresetMessage: 'Vorlage "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||
// Sprungtor
|
||||
jumpGate: 'Sprungtor',
|
||||
jumpGateDescription: 'Nutze das Sprungtor, um Flotten sofort zu einem anderen Mond mit Sprungtor zu transferieren',
|
||||
jumpGateNotAvailable: 'Sprungtor nicht verfügbar',
|
||||
jumpGateRequiresMoon: 'Sprungtor kann nur auf Monden verwendet werden',
|
||||
jumpGateNotBuilt: 'Aktueller Mond hat kein Sprungtor',
|
||||
jumpGateCooldown: 'Sprungtor kühlt ab',
|
||||
jumpGateCooldownRemaining: 'Verbleibende Abklingzeit',
|
||||
jumpGateReady: 'Sprungtor bereit',
|
||||
jumpGateSelectTarget: 'Zielmond auswählen',
|
||||
jumpGateNoTargetMoons: 'Keine verfügbaren Zielmonde (erfordert Sprungtor und abgeschlossene Abklingzeit)',
|
||||
jumpGateSelectFleet: 'Flotte zum Transfer auswählen',
|
||||
jumpGateTransfer: 'Flotte transferieren',
|
||||
jumpGateSuccess: 'Sprungtor-Transfer erfolgreich',
|
||||
jumpGateSuccessMessage: 'Flotte wurde sofort nach {target} transferiert',
|
||||
jumpGateFailed: 'Sprungtor-Transfer fehlgeschlagen',
|
||||
jumpGateFailedMessage: 'Bitte überprüfe den Sprungtor-Status und die Flottenkonfiguration'
|
||||
},
|
||||
officersView: {
|
||||
title: 'Offiziere',
|
||||
@@ -627,6 +697,8 @@ export default {
|
||||
switch: 'Wechseln',
|
||||
recycle: 'Recyceln',
|
||||
debrisField: 'Trümmerfeld',
|
||||
oreDeposits: 'Erzvorkommen',
|
||||
deposits: 'Vorkommen',
|
||||
scoutPlanetTitle: 'Planet ausspionieren',
|
||||
attackPlanetTitle: 'Planet angreifen',
|
||||
missileAttackTitle: 'Raketenangriff',
|
||||
@@ -654,7 +726,9 @@ export default {
|
||||
giftPlanetTitle: 'Geschenk senden',
|
||||
giftPlanetMessage:
|
||||
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.',
|
||||
npcPlanetName: '{name}s Planet'
|
||||
npcPlanetName: '{name}s Planet',
|
||||
intercepted: 'Abgefangen',
|
||||
defenseLosses: 'Verteidigungsverluste'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Nachrichten',
|
||||
@@ -683,6 +757,8 @@ export default {
|
||||
attackerLosses: 'Angreiferverluste',
|
||||
defenderLosses: 'Verteidigerverluste',
|
||||
noLosses: 'Keine Verluste',
|
||||
losses: 'Verluste',
|
||||
remainingUnits: 'Verbleibende Einheiten',
|
||||
plunder: 'Beute',
|
||||
debrisField: 'Trümmerfeld',
|
||||
resources: 'Ressourcen',
|
||||
@@ -700,6 +776,18 @@ export default {
|
||||
round: 'Runde {round}',
|
||||
attackerRemainingPower: 'Verbleibende Angreiferkraft',
|
||||
defenderRemainingPower: 'Verbleibende Verteidigerkraft',
|
||||
// Kampfanimation
|
||||
playAnimation: 'Animation abspielen',
|
||||
showDetails: 'Details anzeigen',
|
||||
speed: 'Geschwindigkeit',
|
||||
power: 'Kampfkraft',
|
||||
battleLogEmpty: 'Kampfprotokoll ist leer',
|
||||
roundStarted: 'Runde {round} gestartet',
|
||||
shipDestroyed: '{count} {ship} zerstört',
|
||||
defenseDestroyed: '{count} {defense} zerstört',
|
||||
attackerWins: 'Angreifer gewinnt',
|
||||
defenderWins: 'Verteidiger gewinnt',
|
||||
roundsPlayed: 'Runden gespielt',
|
||||
missions: 'Missionen',
|
||||
noMissionReports: 'Keine Missionsberichte',
|
||||
success: 'Erfolg',
|
||||
@@ -774,7 +862,8 @@ export default {
|
||||
colonizeSuccess: 'Kolonisierungsmission erfolgreich, neuer Planet gegründet',
|
||||
colonizeFailed: 'Kolonisierungsmission fehlgeschlagen',
|
||||
colonizeFailedOccupied: 'Kolonisierung fehlgeschlagen: Zielposition ist bereits von einem anderen Planeten besetzt',
|
||||
colonizeFailedMaxColonies: 'Kolonisierung fehlgeschlagen: Maximale Anzahl an Kolonien erreicht. Forsche Astrophysik, um das Limit zu erhöhen.',
|
||||
colonizeFailedMaxColonies:
|
||||
'Kolonisierung fehlgeschlagen: Maximale Anzahl an Kolonien erreicht. Forsche Astrophysik, um das Limit zu erhöhen.',
|
||||
spySuccess: 'Spionagemission erfolgreich abgeschlossen',
|
||||
spyFailed: 'Spionagemission fehlgeschlagen',
|
||||
spyFailedTargetNotFound: 'Spionage fehlgeschlagen: Zielplanet existiert nicht',
|
||||
@@ -905,6 +994,7 @@ export default {
|
||||
inAppNotifications: 'In-App-Benachrichtigungen',
|
||||
constructionComplete: 'Bau abgeschlossen',
|
||||
researchComplete: 'Forschung abgeschlossen',
|
||||
unlockNotification: 'Freischaltungsbenachrichtigung',
|
||||
browserPermission: 'Browser-Benachrichtigungen aktivieren',
|
||||
permissionGranted: 'Erlaubnis erteilt',
|
||||
permissionDenied: 'Erlaubnis verweigert/nicht erteilt',
|
||||
@@ -912,11 +1002,22 @@ export default {
|
||||
notificationsDisabled: 'Aktivieren Sie einen der obigen Schalter, um spezifische Benachrichtigungen zu konfigurieren',
|
||||
suppressInFocus: 'Browser-Benachrichtigungen unterdrücken, wenn Seite fokussiert ist',
|
||||
expandTypes: 'Details anzeigen',
|
||||
collapseTypes: 'Details ausblenden'
|
||||
collapseTypes: 'Details ausblenden',
|
||||
// NPC-Namen-Update
|
||||
npcNameUpdate: 'NPC-Namen-Update',
|
||||
npcNameUpdateTitle: 'Alte NPC-Namen erkannt',
|
||||
npcNameUpdateMessage: '{count} NPCs mit altem Namensformat gefunden. Möchten Sie auf neue lokalisierte Namen aktualisieren?',
|
||||
npcNameUpdateConfirm: 'Namen aktualisieren',
|
||||
npcNameUpdateCancel: 'Beibehalten',
|
||||
npcNameUpdateSuccess: '{count} NPC-Namen erfolgreich aktualisiert',
|
||||
npcNameUpdateSkipped: 'NPC-Namen-Update übersprungen'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: 'Bau abgeschlossen',
|
||||
researchComplete: 'Forschung abgeschlossen'
|
||||
researchComplete: 'Forschung abgeschlossen',
|
||||
newUnlock: 'Neuer Inhalt freigeschaltet',
|
||||
building: 'Gebäude',
|
||||
technology: 'Technologie'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GM-Kontrollpanel',
|
||||
@@ -1152,6 +1253,21 @@ export default {
|
||||
attackCooldown: 'Angriff auf Abklingzeit ({min}m {sec}s)',
|
||||
notSpiedYet: 'Noch nicht spioniert, zuerst Spionage nötig'
|
||||
}
|
||||
},
|
||||
aiType: 'KI-Typ',
|
||||
aiTypes: {
|
||||
aggressive: 'Aggressiv',
|
||||
defensive: 'Defensiv',
|
||||
trader: 'Händler',
|
||||
expansionist: 'Expansionist',
|
||||
balanced: 'Ausgewogen'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: 'Spioniert und greift aktiv an, starke Vergeltung',
|
||||
defensive: 'Greift selten an, starke Vergeltung bei Angriff',
|
||||
trader: 'Greift fast nie an, bevorzugt Handel und Geschenke',
|
||||
expansionist: 'Konzentriert auf Entwicklung, weniger aggressiv',
|
||||
balanced: 'Passt Strategie dynamisch an die Situation an'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1313,7 +1429,8 @@ export default {
|
||||
},
|
||||
achievements: {
|
||||
title: 'Erfolgssystem',
|
||||
message: 'Schließen Sie Spielziele ab, um Erfolge freizuschalten und Dunkle Materie-Belohnungen zu erhalten! Erfolge haben mehrere Stufen - streben Sie höhere Herausforderungen an, um bessere Belohnungen zu erhalten.'
|
||||
message:
|
||||
'Schließen Sie Spielziele ab, um Erfolge freizuschalten und Dunkle Materie-Belohnungen zu erhalten! Erfolge haben mehrere Stufen - streben Sie höhere Herausforderungen an, um bessere Belohnungen zu erhalten.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Einstellungen',
|
||||
@@ -1421,5 +1538,24 @@ export default {
|
||||
robbed: 'Von NPC gesammelte Trümmer',
|
||||
lostToNPC: 'An NPC verlorene Trümmerressourcen'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: 'Rangliste',
|
||||
totalPlayers: '{count} Spieler',
|
||||
yourRanking: 'Deine Platzierung',
|
||||
categories: {
|
||||
total: 'Gesamt',
|
||||
building: 'Gebäude',
|
||||
research: 'Forschung',
|
||||
fleet: 'Flotte',
|
||||
defense: 'Verteidigung'
|
||||
},
|
||||
points: 'Pkt',
|
||||
name: 'Name',
|
||||
planets: 'Planeten',
|
||||
details: 'Details',
|
||||
you: 'Du',
|
||||
scoreBreakdown: 'Punkteübersicht',
|
||||
noData: 'Keine Ranglistendaten'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,13 @@ export default {
|
||||
requirementsNotMet: 'Requirements Not Met',
|
||||
current: 'Current',
|
||||
level: 'Level',
|
||||
to: 'to',
|
||||
gmModeActivated: 'GM Mode Activated! Check the navigation menu.',
|
||||
view: 'View',
|
||||
viewDetails: 'View Details',
|
||||
exitConfirmTitle: 'Exit Game',
|
||||
exitConfirmMessage: 'Are you sure you want to exit? Your progress is saved automatically.'
|
||||
exitConfirmMessage: 'Are you sure you want to exit? Your progress is saved automatically.',
|
||||
points: 'Points'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Requirements not met',
|
||||
@@ -84,6 +87,8 @@ export default {
|
||||
galaxy: 'Galaxy',
|
||||
diplomacy: 'Diplomacy',
|
||||
achievements: 'Achievements',
|
||||
campaign: 'Campaign',
|
||||
ranking: 'Ranking',
|
||||
messages: 'Messages',
|
||||
settings: 'Settings',
|
||||
gm: 'GM'
|
||||
@@ -109,7 +114,8 @@ export default {
|
||||
perHour: 'hour',
|
||||
perMinute: 'min',
|
||||
hour: 'hour',
|
||||
noEnergy: 'No Energy'
|
||||
noEnergy: 'No Energy',
|
||||
temperatureBonus: 'Temperature Bonus'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: 'Energy deficit! Resource production stopped!',
|
||||
@@ -119,6 +125,12 @@ export default {
|
||||
deficitDetail: 'Energy deficit: {deficit}, build more power plants',
|
||||
buildSolarPlant: 'Build Power Plant'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: 'Ore deposits running low!',
|
||||
depletedWarning: 'Ore deposits depleted!',
|
||||
depletedResources: 'Depleted: {resources}',
|
||||
lowResources: 'Running low: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: 'Planet',
|
||||
moon: 'Moon',
|
||||
@@ -182,12 +194,20 @@ export default {
|
||||
buildSpeedBonus: 'Build Speed Bonus',
|
||||
researchSpeedBonus: 'Research Speed Bonus',
|
||||
|
||||
missileCapacity: 'Missile Capacity'
|
||||
missileCapacity: 'Missile Capacity',
|
||||
|
||||
// Ore deposits
|
||||
oreDeposit: 'Ore Deposit',
|
||||
remainingDeposit: 'Remaining',
|
||||
depletionTime: 'Est. Depletion',
|
||||
depositDepleted: 'Depleted',
|
||||
depositWarning: 'Warning: Ore deposits are running low (below 10%)!',
|
||||
depositDepletedMessage: 'Ore deposits have been exhausted. Production has stopped.'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Extracts metal resources',
|
||||
crystalMine: 'Extracts crystal resources',
|
||||
deuteriumSynthesizer: 'Synthesizes deuterium resources',
|
||||
deuteriumSynthesizer: 'Synthesizes deuterium (higher output in cold temperatures)',
|
||||
solarPlant: 'Provides energy',
|
||||
fusionReactor: 'Uses deuterium to generate large amounts of energy',
|
||||
roboticsFactory: 'Accelerates construction speed',
|
||||
@@ -237,7 +257,7 @@ export default {
|
||||
colonyShip: 'Used to colonize new planets',
|
||||
recycler: 'Collects debris field resources',
|
||||
espionageProbe: 'Scouts enemy planets',
|
||||
solarSatellite: 'Provides extra energy, generates 50 energy per satellite',
|
||||
solarSatellite: 'Provides extra energy, output based on planet temperature (higher in hot climates)',
|
||||
darkMatterHarvester: 'Special ship for harvesting dark matter',
|
||||
deathstar: 'Ultimate weapon capable of destroying entire planets'
|
||||
},
|
||||
@@ -367,8 +387,16 @@ export default {
|
||||
buildings: 'Buildings',
|
||||
research: 'Research',
|
||||
ships: 'Ships',
|
||||
defense: 'Defense'
|
||||
}
|
||||
defense: 'Defense',
|
||||
waiting: 'Waiting'
|
||||
},
|
||||
waitingEmpty: 'No waiting tasks',
|
||||
addToWaiting: 'Add to Waiting Queue',
|
||||
remove: 'Remove',
|
||||
resourcesReady: 'Ready',
|
||||
waitingResources: 'Waiting',
|
||||
waitingQueueFull: 'Waiting queue is full',
|
||||
movedToQueue: 'Task moved to queue'
|
||||
},
|
||||
overview: {
|
||||
title: 'Planet Overview',
|
||||
@@ -381,7 +409,10 @@ export default {
|
||||
consumptionSourcesDesc: 'Energy consumption details for buildings',
|
||||
totalProduction: 'Total Production',
|
||||
totalConsumption: 'Total Consumption',
|
||||
noConsumption: 'No energy consumption'
|
||||
noConsumption: 'No energy consumption',
|
||||
tabOverview: 'Overview',
|
||||
tabProduction: 'Production Details',
|
||||
tabConsumption: 'Consumption Details'
|
||||
},
|
||||
buildingsView: {
|
||||
title: 'Buildings',
|
||||
@@ -510,6 +541,29 @@ export default {
|
||||
spy: 'Spy',
|
||||
deploy: 'Deploy',
|
||||
expedition: 'Expedition',
|
||||
expeditionZone: 'Expedition Zone',
|
||||
expeditionZoneDesc: 'Select destination zone. Different zones have different risks and rewards',
|
||||
requiresAstro: 'Requires Astrophysics level {level}',
|
||||
reward: 'Reward',
|
||||
danger: 'Danger',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: 'Near Space',
|
||||
desc: 'Safe near-space area, low risk but fewer rewards'
|
||||
},
|
||||
deepSpace: {
|
||||
name: 'Deep Space',
|
||||
desc: 'Far from stars, more resources may be found'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: 'Uncharted Space',
|
||||
desc: 'Unexplored area, high risk high reward'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: 'Dangerous Nebula',
|
||||
desc: 'Nebula full of unknown dangers, but contains extremely rich treasures'
|
||||
}
|
||||
},
|
||||
recycle: 'Recycle',
|
||||
destroy: 'Planet Destruction',
|
||||
transportResources: 'Transport Resources',
|
||||
@@ -645,6 +699,8 @@ export default {
|
||||
switch: 'Switch',
|
||||
recycle: 'Recycle',
|
||||
debrisField: 'Debris Field',
|
||||
oreDeposits: 'Ore Deposits',
|
||||
deposits: 'Deposits',
|
||||
scoutPlanetTitle: 'Scout Planet',
|
||||
attackPlanetTitle: 'Attack Planet',
|
||||
missileAttackTitle: 'Missile Attack',
|
||||
@@ -690,7 +746,9 @@ export default {
|
||||
phalanxStatus: 'Status',
|
||||
phalanxStatusOutbound: 'Outbound',
|
||||
phalanxStatusReturning: 'Returning',
|
||||
phalanxInsufficientDeuterium: 'Insufficient Deuterium'
|
||||
phalanxInsufficientDeuterium: 'Insufficient Deuterium',
|
||||
intercepted: 'Intercepted',
|
||||
defenseLosses: 'Defense Losses'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Messages',
|
||||
@@ -714,6 +772,8 @@ export default {
|
||||
attackerLosses: 'Attacker Losses',
|
||||
defenderLosses: 'Defender Losses',
|
||||
noLosses: 'No losses',
|
||||
losses: 'Losses',
|
||||
remainingUnits: 'Remaining Units',
|
||||
plunder: 'Plunder',
|
||||
debrisField: 'Debris Field',
|
||||
resources: 'Resources',
|
||||
@@ -872,6 +932,18 @@ export default {
|
||||
round: 'Round {round}',
|
||||
attackerRemainingPower: 'Attacker remaining power',
|
||||
defenderRemainingPower: 'Defender remaining power',
|
||||
// Battle animation
|
||||
playAnimation: 'Play Animation',
|
||||
showDetails: 'Show Details',
|
||||
speed: 'Speed',
|
||||
power: 'Power',
|
||||
battleLogEmpty: 'Battle log is empty',
|
||||
roundStarted: 'Round {round} started',
|
||||
shipDestroyed: '{count} {ship} destroyed',
|
||||
defenseDestroyed: '{count} {defense} destroyed',
|
||||
attackerWins: 'Attacker Wins',
|
||||
defenderWins: 'Defender Wins',
|
||||
roundsPlayed: 'rounds played',
|
||||
spied: 'Spied',
|
||||
spiedNotification: 'Spied Notification',
|
||||
noSpiedNotifications: 'No spied notifications',
|
||||
@@ -943,6 +1015,7 @@ export default {
|
||||
inAppNotifications: 'In-App Notifications',
|
||||
constructionComplete: 'Construction Complete',
|
||||
researchComplete: 'Research Complete',
|
||||
unlockNotification: 'Unlock Notification',
|
||||
browserPermission: 'Enable Browser Notifications',
|
||||
permissionGranted: 'Permission Granted',
|
||||
permissionDenied: 'Permission Denied/Not Granted',
|
||||
@@ -950,11 +1023,22 @@ export default {
|
||||
notificationsDisabled: 'Enable any switch above to configure specific notifications',
|
||||
suppressInFocus: 'Suppress browser notifications when page is focused',
|
||||
expandTypes: 'Expand Details',
|
||||
collapseTypes: 'Collapse Details'
|
||||
collapseTypes: 'Collapse Details',
|
||||
// NPC name update
|
||||
npcNameUpdate: 'NPC Name Update',
|
||||
npcNameUpdateTitle: 'Old NPC Names Detected',
|
||||
npcNameUpdateMessage: 'Found {count} NPCs using old name format. Would you like to update them to new localized names?',
|
||||
npcNameUpdateConfirm: 'Update Names',
|
||||
npcNameUpdateCancel: 'Keep Current',
|
||||
npcNameUpdateSuccess: 'Successfully updated {count} NPC names',
|
||||
npcNameUpdateSkipped: 'NPC name update skipped'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: 'Construction Complete',
|
||||
researchComplete: 'Research Complete'
|
||||
researchComplete: 'Research Complete',
|
||||
newUnlock: 'New Content Unlocked',
|
||||
building: 'Building',
|
||||
technology: 'Technology'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GM Control Panel',
|
||||
@@ -1193,6 +1277,21 @@ export default {
|
||||
attackCooldown: 'Attack on cooldown ({min}m {sec}s)',
|
||||
notSpiedYet: 'Not yet spied, need to spy first'
|
||||
}
|
||||
},
|
||||
aiType: 'AI Type',
|
||||
aiTypes: {
|
||||
aggressive: 'Aggressive',
|
||||
defensive: 'Defensive',
|
||||
trader: 'Trader',
|
||||
expansionist: 'Expansionist',
|
||||
balanced: 'Balanced'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: 'Actively spies and attacks, strong retaliation',
|
||||
defensive: 'Rarely attacks, strong retaliation when attacked',
|
||||
trader: 'Almost never attacks, prefers trading and gifts',
|
||||
expansionist: 'Focuses on development, less aggressive',
|
||||
balanced: 'Dynamically adjusts strategy based on situation'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1470,7 +1569,8 @@ export default {
|
||||
},
|
||||
achievements: {
|
||||
title: 'Achievement System',
|
||||
message: 'Complete game objectives to unlock achievements and earn Dark Matter rewards! Achievements have multiple tiers - aim for higher challenges to get better rewards.'
|
||||
message:
|
||||
'Complete game objectives to unlock achievements and earn Dark Matter rewards! Achievements have multiple tiers - aim for higher challenges to get better rewards.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
@@ -1578,5 +1678,339 @@ export default {
|
||||
robbed: 'Times debris recycled by NPC',
|
||||
lostToNPC: 'Total debris resources lost to NPC'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: 'Ranking',
|
||||
totalPlayers: '{count} Players',
|
||||
yourRanking: 'Your Ranking',
|
||||
categories: {
|
||||
total: 'Total',
|
||||
building: 'Building',
|
||||
research: 'Research',
|
||||
fleet: 'Fleet',
|
||||
defense: 'Defense'
|
||||
},
|
||||
points: 'pts',
|
||||
name: 'Name',
|
||||
planets: 'Planets',
|
||||
details: 'Details',
|
||||
you: 'You',
|
||||
scoreBreakdown: 'Score Breakdown',
|
||||
noData: 'No ranking data'
|
||||
},
|
||||
campaign: {
|
||||
name: 'Campaign',
|
||||
description: 'Explore the mysterious galaxy and uncover ancient secrets',
|
||||
totalProgress: 'Total Progress',
|
||||
questsCompleted: 'Quests Completed',
|
||||
chapter: 'Chapter',
|
||||
branch: 'Branch',
|
||||
startQuest: 'Start Quest',
|
||||
claimRewards: 'Claim Rewards',
|
||||
objectives: 'Objectives',
|
||||
objectivesLabel: 'Goals',
|
||||
rewards: 'Rewards',
|
||||
completed: 'Completed',
|
||||
inProgress: 'In Progress',
|
||||
available: 'Available',
|
||||
locked: 'Locked',
|
||||
notifications: {
|
||||
questStarted: 'Quest started',
|
||||
questCompleted: 'Quest completed!',
|
||||
rewardsClaimed: 'Rewards claimed',
|
||||
objectiveCompleted: 'Objective completed',
|
||||
chapterUnlocked: 'New chapter unlocked'
|
||||
},
|
||||
dialogue: {
|
||||
skip: 'Skip',
|
||||
continue: 'Continue',
|
||||
finish: 'Finish',
|
||||
player: 'Commander',
|
||||
npc: 'NPC',
|
||||
narrator: 'Narrator',
|
||||
mysterious: 'Mysterious Signal',
|
||||
unknownSource: 'Unknown source'
|
||||
},
|
||||
chapters: {
|
||||
'1': {
|
||||
title: 'Origin',
|
||||
description: 'Build your home and take the first step into space',
|
||||
backgroundStory: 'You are a young space commander who has just acquired your first planet. In this vast universe, you will build your home, develop technology, and explore the depths of the galaxy...'
|
||||
},
|
||||
'2': {
|
||||
title: 'Exploration',
|
||||
description: 'Explore the universe and discover ancient ruins',
|
||||
backgroundStory: 'As your power grows, mysterious signals from deep space catch your attention. These signals seem to point to an ancient secret, waiting for brave explorers to uncover...'
|
||||
},
|
||||
'3': {
|
||||
title: 'Diplomacy',
|
||||
description: 'Establish connections with other factions',
|
||||
backgroundStory: 'You are not alone in the galaxy. Other civilizations are rising. You must decide whether to be their enemy or ally. Diplomatic wisdom will determine how far your empire can go...'
|
||||
},
|
||||
'4': {
|
||||
title: 'Shadow Rising',
|
||||
description: 'Face powerful enemies and defend your territory',
|
||||
backgroundStory: 'Danger lurks in the shadows. A powerful hostile force has targeted your territory. War is inevitable. You must prepare to face the coming storm...'
|
||||
},
|
||||
'5': {
|
||||
title: 'Ancient Secrets',
|
||||
description: 'Uncover the deepest secrets of the galaxy',
|
||||
backgroundStory: 'All clues point to the most mysterious region of the galaxy. There, the ultimate secrets left by ancient civilizations await. Are you ready to uncover everything?'
|
||||
}
|
||||
},
|
||||
quests: {
|
||||
'1_1': {
|
||||
title: 'Home Building',
|
||||
description: 'Build infrastructure to lay the foundation for your planet'
|
||||
},
|
||||
'1_2': {
|
||||
title: 'Tech Enlightenment',
|
||||
description: 'Research basic technology to begin your tech journey'
|
||||
},
|
||||
'1_3': {
|
||||
title: 'First Ship',
|
||||
description: 'Build your first warship'
|
||||
},
|
||||
'1_4': {
|
||||
title: 'Strange Neighbors',
|
||||
description: 'Scout other factions in nearby systems'
|
||||
},
|
||||
'1_5': {
|
||||
title: 'First Contact',
|
||||
description: 'Establish initial contact with nearby NPC factions'
|
||||
},
|
||||
'2_1': {
|
||||
title: 'Pioneer Colony',
|
||||
description: 'Colonize your first new planet'
|
||||
},
|
||||
'2_2': {
|
||||
title: 'Deep Space Expedition',
|
||||
description: 'Send fleet on expedition missions'
|
||||
},
|
||||
'2_3': {
|
||||
title: 'Mysterious Signal',
|
||||
description: 'Investigate mysterious signals from deep space'
|
||||
},
|
||||
'2_4': {
|
||||
title: 'Ruin Investigation',
|
||||
description: 'Explore discovered ancient ruins'
|
||||
},
|
||||
'2_5': {
|
||||
title: 'Decrypt Archives',
|
||||
description: 'Research data obtained from ruins'
|
||||
},
|
||||
'3_1': {
|
||||
title: 'Peacemaker',
|
||||
description: 'Improve relations with NPCs through diplomacy'
|
||||
},
|
||||
'3_2': {
|
||||
title: 'Trade Relations',
|
||||
description: 'Establish stable relations with friendly factions'
|
||||
},
|
||||
'3_3': {
|
||||
title: 'Common Threat',
|
||||
description: 'Discover potential hostile forces'
|
||||
},
|
||||
'3_4': {
|
||||
title: 'Alliance Negotiations',
|
||||
description: 'Form a formal alliance with friendly NPCs'
|
||||
},
|
||||
'3_5': {
|
||||
title: 'Storm Preparation',
|
||||
description: 'Build defenses to prepare for challenges'
|
||||
},
|
||||
'4_1': {
|
||||
title: 'Outpost Attack',
|
||||
description: 'Repel the first attack from hostile forces'
|
||||
},
|
||||
'4_2': {
|
||||
title: 'Intelligence Gathering',
|
||||
description: 'Scout enemy military deployments'
|
||||
},
|
||||
'4_3': {
|
||||
title: 'Counterattack',
|
||||
description: 'Launch a counterattack against the enemy'
|
||||
},
|
||||
'4_4': {
|
||||
title: 'Resource Contest',
|
||||
description: 'Recycle battlefield debris for resources'
|
||||
},
|
||||
'4_5': {
|
||||
title: 'Eve of Battle',
|
||||
description: 'Build a powerful fleet for the final battle'
|
||||
},
|
||||
'5_1': {
|
||||
title: 'Depths of Ruins',
|
||||
description: 'Explore the deepest parts of the ruins'
|
||||
},
|
||||
'5_2': {
|
||||
title: 'Ancient Technology',
|
||||
description: 'Unlock ancient civilization technology'
|
||||
},
|
||||
'5_3': {
|
||||
title: 'Final Confrontation',
|
||||
description: 'Face the final battle against the mysterious enemy'
|
||||
},
|
||||
'5_4': {
|
||||
title: 'New Era',
|
||||
description: 'Establish new colonies and begin a new age'
|
||||
},
|
||||
'5_5': {
|
||||
title: 'Legacy Continues',
|
||||
description: 'Continue to develop and conquer more systems'
|
||||
}
|
||||
},
|
||||
objectiveTypes: {
|
||||
buildBuilding: 'Build {building} to level {level}',
|
||||
researchTech: 'Research {tech} to level {level}',
|
||||
produceShips: 'Produce {count} {ship}',
|
||||
accumulateResources: 'Accumulate {amount} {resource}',
|
||||
defeatNPC: 'Defeat {npc}',
|
||||
winBattles: 'Win {count} battles',
|
||||
recycleDebris: 'Recycle {amount} debris',
|
||||
reachRelation: 'Reach {level} relation with {npc}',
|
||||
sendGift: 'Send {count} gifts to {npc}',
|
||||
formAlliance: 'Form alliance with {npc}',
|
||||
colonize: 'Colonize {count} planets',
|
||||
expedition: 'Complete {count} expeditions',
|
||||
spyTarget: 'Spy on {target}'
|
||||
},
|
||||
errors: {
|
||||
questNotFound: 'Quest not found',
|
||||
questNotAvailable: 'Quest not available',
|
||||
questNotActive: 'Quest not active',
|
||||
questNotCompleted: 'Quest not completed',
|
||||
rewardsAlreadyClaimed: 'Rewards already claimed',
|
||||
prerequisiteNotMet: 'Prerequisite quest not completed'
|
||||
},
|
||||
speakers: {
|
||||
ancientVoice: 'Ancient Voice',
|
||||
neighborNPC: 'Neighbor Faction',
|
||||
mysteriousSignal: 'Mysterious Signal',
|
||||
enemyCommander: 'Enemy Commander'
|
||||
},
|
||||
objectiveDescriptions: {
|
||||
buildMetalMine: 'Build Metal Mine to level 2',
|
||||
buildCrystalMine: 'Build Crystal Mine to level 2',
|
||||
buildSolarPlant: 'Build Solar Plant to level 2',
|
||||
buildResearchLab: 'Build Research Lab to level 1',
|
||||
researchEnergy: 'Research Energy Technology to level 1',
|
||||
buildShipyard: 'Build Shipyard to level 2',
|
||||
researchCombustion: 'Research Combustion Drive to level 1',
|
||||
buildLightFighters: 'Build 5 Light Fighters',
|
||||
researchEspionage: 'Research Espionage Technology to level 2',
|
||||
buildSpyProbes: 'Build 3 Espionage Probes',
|
||||
spyAnyNPC: 'Spy on any NPC planet',
|
||||
sendGiftToNPC: 'Send a gift to any NPC',
|
||||
researchAstrophysics: 'Research Astrophysics to level 1',
|
||||
buildColonyShip: 'Build a Colony Ship',
|
||||
colonizeNewPlanet: 'Colonize a new planet',
|
||||
completeExpedition: 'Complete an expedition mission',
|
||||
discoverRuins: 'Discover ancient ruins',
|
||||
researchComputer: 'Research Computer Technology to level 4',
|
||||
improveRelation: 'Improve relations with an NPC',
|
||||
reachFriendly: 'Reach friendly status with an NPC',
|
||||
spyHostileNPC: 'Spy on a hostile NPC',
|
||||
formAlliance: 'Form alliance with a friendly NPC',
|
||||
buildDefenses: 'Build defense facilities',
|
||||
winDefenseBattle: 'Win a defensive battle',
|
||||
spyEnemyPlanet: 'Spy on enemy planet',
|
||||
attackEnemy: 'Attack the enemy',
|
||||
recycleDebris: 'Recycle debris',
|
||||
buildBattleships: 'Build 10 Battleships',
|
||||
exploreDeepRuins: 'Explore deep ruins',
|
||||
researchHyperspace: 'Research Hyperspace Technology',
|
||||
defeatBoss: 'Defeat the mysterious enemy',
|
||||
colonizeSpecial: 'Colonize special location',
|
||||
continueDevelopment: 'Continue development'
|
||||
},
|
||||
dialogues: {
|
||||
'1_1': {
|
||||
prologue_1: 'Welcome to the galaxy, young commander. This vast universe awaits your exploration. First, let us build up your home planet.',
|
||||
prologue_2: 'I sense a new consciousness awakening... Interesting... Let us see how far you can go...'
|
||||
},
|
||||
'1_2': {
|
||||
prologue_1: 'Basic infrastructure is complete. Now it is time to develop technology. Build a Research Lab and begin your tech journey.'
|
||||
},
|
||||
'1_3': {
|
||||
prologue_1: 'With technology support, you can start building your fleet. Build a Shipyard and produce your first warship.'
|
||||
},
|
||||
'1_4': {
|
||||
prologue_1: 'Your fleet is taking shape. Now let us learn about your surroundings. Send out spy probes to scout nearby factions.',
|
||||
prologue_2: 'You are not alone... Other civilizations exist in this galaxy...'
|
||||
},
|
||||
'1_5': {
|
||||
prologue_1: 'You have discovered nearby factions. Diplomacy is an art. Try to establish contact with them.',
|
||||
epilogue_1: 'Thank you for your gift, commander. I hope we can become friends.',
|
||||
epilogue_2: 'Good... Establishing connections is the first step to uncovering deeper secrets...'
|
||||
},
|
||||
'2_1': {
|
||||
prologue_1: 'Your power is established. It is time to expand your territory. Research Astrophysics, build a colony ship, and explore new planets.',
|
||||
prologue_2: 'The universe is infinite... More planets mean more possibilities...'
|
||||
},
|
||||
'2_2': {
|
||||
prologue_1: 'Colonization successful! But deeper secrets await in the universe. Send your fleet on expedition missions.',
|
||||
prologue_2: 'Faint signals from afar... Something awaits you there...'
|
||||
},
|
||||
'2_3': {
|
||||
prologue_1: 'Your expedition discovered anomalous signals. These signals seem to come from an ancient civilization... Investigate their source.',
|
||||
epilogue_1: 'These symbols... They are ruins of an ancient civilization! Continue investigating to uncover their secrets.'
|
||||
},
|
||||
'2_4': {
|
||||
prologue_1: 'You have found the location of ancient ruins. Send your fleet to explore and see what you can discover.'
|
||||
},
|
||||
'2_5': {
|
||||
prologue_1: 'Data archives were found in the ruins. Study this data, perhaps you can unlock new technology.'
|
||||
},
|
||||
'3_1': {
|
||||
prologue_1: 'While exploring, do not forget about diplomacy. Maintaining good relations with surrounding factions benefits you.'
|
||||
},
|
||||
'3_2': {
|
||||
prologue_1: 'Some factions have shown friendliness. Continue deepening relations, perhaps you can gain more support.'
|
||||
},
|
||||
'3_3': {
|
||||
prologue_1: 'Intelligence indicates hostile forces are watching you from the shadows. Stay vigilant and scout their movements.'
|
||||
},
|
||||
'3_4': {
|
||||
prologue_1: 'Establish a formal alliance with friendly factions to support each other against threats.'
|
||||
},
|
||||
'3_5': {
|
||||
prologue_1: 'Threats are approaching. Build defense facilities and prepare for possible conflict.'
|
||||
},
|
||||
'4_1': {
|
||||
prologue_1: 'The enemy has launched an attack! Defend your planet!',
|
||||
epilogue_1: 'You successfully repelled the enemy\'s first wave. But this is just the beginning...'
|
||||
},
|
||||
'4_2': {
|
||||
prologue_1: 'The enemy has retreated, but they will return. Scout their planets to understand their strength.'
|
||||
},
|
||||
'4_3': {
|
||||
prologue_1: 'It is time to strike back. Attack the enemy planets and weaken their forces.'
|
||||
},
|
||||
'4_4': {
|
||||
prologue_1: 'Much debris remains on the battlefield. Recycle these resources to prepare for the next battle.'
|
||||
},
|
||||
'4_5': {
|
||||
prologue_1: 'The final battle approaches. Build a powerful fleet and prepare for the ultimate challenge.'
|
||||
},
|
||||
'5_1': {
|
||||
prologue_1: 'All clues point to the deepest part of the ruins. The core secrets of the ancient civilization lie there.',
|
||||
prologue_2: 'You have finally arrived... The truth will soon be revealed...'
|
||||
},
|
||||
'5_2': {
|
||||
prologue_1: 'In the depths of the ruins, you discovered lost ancient technology. Research and unlock their power.'
|
||||
},
|
||||
'5_3': {
|
||||
prologue_1: 'A mysterious enemy has appeared. This is the final challenge. Defeat it!',
|
||||
epilogue_1: 'You did it! The ancient guardian has been defeated. The secrets of the galaxy are now open to you.'
|
||||
},
|
||||
'5_4': {
|
||||
prologue_1: 'Peace has finally arrived. In this new era, establish new colonies and expand your empire.'
|
||||
},
|
||||
'5_5': {
|
||||
prologue_1: 'Your legend has just begun. Continue exploring and conquering more star systems!',
|
||||
epilogue_1: 'The galaxy is vast and boundless, with countless secrets waiting for you to discover...'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,13 @@ export default {
|
||||
requirementsNotMet: '必要条件が満たされていません',
|
||||
current: '現在',
|
||||
level: 'レベル',
|
||||
to: '〜',
|
||||
gmModeActivated: 'GMモードが有効になりました!ナビゲーションメニューをご確認ください。',
|
||||
view: '表示',
|
||||
viewDetails: '詳細を見る',
|
||||
exitConfirmTitle: 'ゲーム終了',
|
||||
exitConfirmMessage: 'ゲームを終了しますか?進行状況は自動的に保存されます。'
|
||||
exitConfirmMessage: 'ゲームを終了しますか?進行状況は自動的に保存されます。',
|
||||
points: 'ポイント'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '前提条件を満たしていません',
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
galaxy: '銀河',
|
||||
diplomacy: '外交',
|
||||
achievements: '実績',
|
||||
ranking: 'ランキング',
|
||||
messages: 'メッセージ',
|
||||
settings: '設定',
|
||||
guide: 'ゲームガイド',
|
||||
@@ -111,7 +115,8 @@ export default {
|
||||
perHour: '時間',
|
||||
perMinute: '分',
|
||||
hour: '時間',
|
||||
noEnergy: 'エネルギー不足'
|
||||
noEnergy: 'エネルギー不足',
|
||||
temperatureBonus: '温度ボーナス'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: 'エネルギー不足!資源生産が停止しています!',
|
||||
@@ -121,6 +126,12 @@ export default {
|
||||
deficitDetail: 'エネルギー不足: {deficit}、発電所を建設してください',
|
||||
buildSolarPlant: '発電所を建設'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: '鉱床が減少しています!',
|
||||
depletedWarning: '鉱床が枯渇しました!',
|
||||
depletedResources: '枯渇: {resources}',
|
||||
lowResources: '残りわずか: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: '惑星',
|
||||
moon: '月',
|
||||
@@ -187,12 +198,20 @@ export default {
|
||||
researchSpeedBonus: '研究速度ボーナス',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
missileCapacity: 'Missile Capacity',
|
||||
|
||||
// 鉱脈埋蔵量
|
||||
oreDeposit: '鉱脈埋蔵量',
|
||||
remainingDeposit: '残り',
|
||||
depletionTime: '枯渇予測',
|
||||
depositDepleted: '枯渇',
|
||||
depositWarning: '警告:鉱脈埋蔵量が残りわずか(10%以下)!',
|
||||
depositDepletedMessage: '鉱脈が枯渇しました。生産が停止しています。'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '金属資源を採掘',
|
||||
crystalMine: 'クリスタル資源を採掘',
|
||||
deuteriumSynthesizer: '重水素資源を合成',
|
||||
deuteriumSynthesizer: '重水素資源を合成(低温ほど産出量増加)',
|
||||
solarPlant: 'エネルギーを供給',
|
||||
fusionReactor: '重水素を使用して大量のエネルギーを生成',
|
||||
roboticsFactory: '建設速度を向上',
|
||||
@@ -242,7 +261,7 @@ export default {
|
||||
colonyShip: '新惑星の植民に使用',
|
||||
recycler: 'デブリフィールドの資源を回収',
|
||||
espionageProbe: '敵惑星を偵察',
|
||||
solarSatellite: '追加エネルギーを提供、衛星1つにつき50エネルギー生成',
|
||||
solarSatellite: '追加エネルギーを提供、惑星温度により産出量変動(高温ほど高産出)',
|
||||
darkMatterHarvester: 'ダークマター採取専用の特殊艦',
|
||||
deathstar: '惑星全体を破壊できる究極兵器'
|
||||
},
|
||||
@@ -367,8 +386,16 @@ export default {
|
||||
buildings: '建物',
|
||||
research: '研究',
|
||||
ships: '艦船',
|
||||
defense: '防衛'
|
||||
}
|
||||
defense: '防衛',
|
||||
waiting: '待機'
|
||||
},
|
||||
waitingEmpty: '待機中のタスクはありません',
|
||||
addToWaiting: '待機キューに追加',
|
||||
remove: '削除',
|
||||
resourcesReady: '準備完了',
|
||||
waitingResources: '待機中',
|
||||
waitingQueueFull: '待機キューが満杯です',
|
||||
movedToQueue: 'タスクがキューに移動しました'
|
||||
},
|
||||
shipyard: {
|
||||
attack: '攻撃力',
|
||||
@@ -398,7 +425,10 @@ export default {
|
||||
consumptionSourcesDesc: '建物のエネルギー消費詳細',
|
||||
totalProduction: '総生産量',
|
||||
totalConsumption: '総消費量',
|
||||
noConsumption: 'エネルギー消費なし'
|
||||
noConsumption: 'エネルギー消費なし',
|
||||
tabOverview: '概要',
|
||||
tabProduction: '生産詳細',
|
||||
tabConsumption: '消費詳細'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '建物',
|
||||
@@ -513,6 +543,29 @@ export default {
|
||||
spy: '偵察',
|
||||
deploy: '配備',
|
||||
expedition: '探検',
|
||||
expeditionZone: '探検エリア',
|
||||
expeditionZoneDesc: '目的地を選択してください。エリアによってリスクと報酬が異なります',
|
||||
requiresAstro: '宇宙物理学レベル {level} が必要',
|
||||
reward: '報酬',
|
||||
danger: '危険',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: '近宇宙',
|
||||
desc: '安全な近宇宙、リスクは低いが報酬も少ない'
|
||||
},
|
||||
deepSpace: {
|
||||
name: '深宇宙',
|
||||
desc: '恒星から遠く離れた場所、より多くの資源が見つかる可能性'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: '未知の宇宙',
|
||||
desc: '未探索のエリア、ハイリスク・ハイリターン'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: '危険な星雲',
|
||||
desc: '未知の危険に満ちた星雲、しかし非常に豊かな宝物を含む'
|
||||
}
|
||||
},
|
||||
recycle: '回収',
|
||||
transportResources: '資源輸送',
|
||||
totalCargoCapacity: '総積載量',
|
||||
@@ -567,7 +620,24 @@ export default {
|
||||
presetName: 'プリセット名',
|
||||
presetNamePlaceholder: 'プリセット名を入力',
|
||||
deletePresetTitle: 'プリセット削除',
|
||||
deletePresetMessage: 'プリセット「{name}」を削除しますか?この操作は取り消せません。'
|
||||
deletePresetMessage: 'プリセット「{name}」を削除しますか?この操作は取り消せません。',
|
||||
// ジャンプゲート
|
||||
jumpGate: 'ジャンプゲート',
|
||||
jumpGateDescription: 'ジャンプゲートを使用して、他のジャンプゲートがある月へ艦隊を瞬時に転送',
|
||||
jumpGateNotAvailable: 'ジャンプゲート使用不可',
|
||||
jumpGateRequiresMoon: 'ジャンプゲートは月でのみ使用可能',
|
||||
jumpGateNotBuilt: '現在の月にジャンプゲートがありません',
|
||||
jumpGateCooldown: 'ジャンプゲートクールダウン中',
|
||||
jumpGateCooldownRemaining: '残りクールダウン時間',
|
||||
jumpGateReady: 'ジャンプゲート準備完了',
|
||||
jumpGateSelectTarget: '目標の月を選択',
|
||||
jumpGateNoTargetMoons: '利用可能な目標の月がありません(ジャンプゲートとクールダウン完了が必要)',
|
||||
jumpGateSelectFleet: '転送する艦隊を選択',
|
||||
jumpGateTransfer: '艦隊を転送',
|
||||
jumpGateSuccess: 'ジャンプゲート転送成功',
|
||||
jumpGateSuccessMessage: '艦隊は{target}へ瞬時に転送されました',
|
||||
jumpGateFailed: 'ジャンプゲート転送失敗',
|
||||
jumpGateFailedMessage: 'ジャンプゲートの状態と艦隊構成を確認してください'
|
||||
},
|
||||
officersView: {
|
||||
title: '士官',
|
||||
@@ -626,6 +696,8 @@ export default {
|
||||
switch: '切り替え',
|
||||
recycle: '回収',
|
||||
debrisField: 'デブリフィールド',
|
||||
oreDeposits: '鉱脈埋蔵量',
|
||||
deposits: '埋蔵量',
|
||||
scoutPlanetTitle: '惑星偵察',
|
||||
attackPlanetTitle: '惑星攻撃',
|
||||
missileAttackTitle: 'ミサイル攻撃',
|
||||
@@ -648,7 +720,9 @@ export default {
|
||||
debris: '破片',
|
||||
giftPlanetTitle: 'ギフト送信',
|
||||
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。',
|
||||
npcPlanetName: '{name}の惑星'
|
||||
npcPlanetName: '{name}の惑星',
|
||||
intercepted: '迎撃済み',
|
||||
defenseLosses: '防衛損失'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'メッセージセンター',
|
||||
@@ -672,6 +746,8 @@ export default {
|
||||
attackerLosses: '攻撃側損失',
|
||||
defenderLosses: '防御側損失',
|
||||
noLosses: '損失なし',
|
||||
losses: '損失',
|
||||
remainingUnits: '残存ユニット',
|
||||
plunder: '略奪資源',
|
||||
debrisField: 'デブリフィールド',
|
||||
resources: '資源',
|
||||
@@ -689,6 +765,18 @@ export default {
|
||||
round: '第{round}ラウンド',
|
||||
attackerRemainingPower: '攻撃側残存火力',
|
||||
defenderRemainingPower: '防御側残存火力',
|
||||
// 戦闘アニメーション
|
||||
playAnimation: 'アニメーション再生',
|
||||
showDetails: '詳細表示',
|
||||
speed: '速度',
|
||||
power: '戦闘力',
|
||||
battleLogEmpty: '戦闘ログは空です',
|
||||
roundStarted: '第{round}ラウンド開始',
|
||||
shipDestroyed: '{ship}が{count}隻撃破',
|
||||
defenseDestroyed: '{defense}が{count}基撃破',
|
||||
attackerWins: '攻撃側勝利',
|
||||
defenderWins: '防御側勝利',
|
||||
roundsPlayed: 'ラウンド完了',
|
||||
spied: '偵察された',
|
||||
spiedNotification: '偵察通知',
|
||||
noSpiedNotifications: '偵察通知はありません',
|
||||
@@ -897,6 +985,7 @@ export default {
|
||||
inAppNotifications: 'アプリ内通知',
|
||||
constructionComplete: '建設完了',
|
||||
researchComplete: '研究完了',
|
||||
unlockNotification: '解放通知',
|
||||
browserPermission: 'ブラウザ通知を有効にする',
|
||||
permissionGranted: '許可されました',
|
||||
permissionDenied: '許可が拒否されたか、付与されていません',
|
||||
@@ -904,11 +993,22 @@ export default {
|
||||
notificationsDisabled: '特定の通知を設定するには、上記のスイッチを有効にしてください',
|
||||
suppressInFocus: 'ページにフォーカスがある場合、ブラウザ通知を抑制する',
|
||||
expandTypes: '詳細を展開',
|
||||
collapseTypes: '詳細を折りたたむ'
|
||||
collapseTypes: '詳細を折りたたむ',
|
||||
// NPC名前更新
|
||||
npcNameUpdate: 'NPC名前更新',
|
||||
npcNameUpdateTitle: '旧形式のNPC名を検出',
|
||||
npcNameUpdateMessage: '{count}件のNPCが旧形式の名前を使用しています。新しいローカライズ名に更新しますか?',
|
||||
npcNameUpdateConfirm: '名前を更新',
|
||||
npcNameUpdateCancel: '現状維持',
|
||||
npcNameUpdateSuccess: '{count}件のNPC名を更新しました',
|
||||
npcNameUpdateSkipped: 'NPC名の更新をスキップしました'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: '建設完了',
|
||||
researchComplete: '研究完了'
|
||||
researchComplete: '研究完了',
|
||||
newUnlock: '新コンテンツ解放',
|
||||
building: '建物',
|
||||
technology: '技術'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GMコントロールパネル',
|
||||
@@ -1144,6 +1244,21 @@ export default {
|
||||
attackCooldown: '攻撃クールダウン中({min}分{sec}秒)',
|
||||
notSpiedYet: '未偵察、先に偵察が必要'
|
||||
}
|
||||
},
|
||||
aiType: 'AIタイプ',
|
||||
aiTypes: {
|
||||
aggressive: '攻撃型',
|
||||
defensive: '防御型',
|
||||
trader: '商人型',
|
||||
expansionist: '拡張型',
|
||||
balanced: 'バランス型'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: '積極的に偵察・攻撃、強い報復',
|
||||
defensive: '滅多に攻撃しない、攻撃されると強く報復',
|
||||
trader: 'ほとんど攻撃しない、取引と贈り物を好む',
|
||||
expansionist: '発展に集中、攻撃性が低い',
|
||||
balanced: '状況に応じて戦略を動的に調整'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1295,7 +1410,8 @@ export default {
|
||||
},
|
||||
achievements: {
|
||||
title: '実績システム',
|
||||
message: 'ゲーム目標を達成して実績をアンロックし、ダークマター報酬を獲得!実績には複数のティアがあり、高い目標に挑戦してより良い報酬を手に入れましょう。'
|
||||
message:
|
||||
'ゲーム目標を達成して実績をアンロックし、ダークマター報酬を獲得!実績には複数のティアがあり、高い目標に挑戦してより良い報酬を手に入れましょう。'
|
||||
},
|
||||
settings: {
|
||||
title: '設定',
|
||||
@@ -1402,5 +1518,24 @@ export default {
|
||||
robbed: 'NPCに残骸を回収された回数',
|
||||
lostToNPC: 'NPCに奪われた残骸資源総量'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: 'ランキング',
|
||||
totalPlayers: '{count} プレイヤー',
|
||||
yourRanking: 'あなたの順位',
|
||||
categories: {
|
||||
total: '総合',
|
||||
building: '建設',
|
||||
research: '研究',
|
||||
fleet: '艦隊',
|
||||
defense: '防衛'
|
||||
},
|
||||
points: 'pt',
|
||||
name: '名前',
|
||||
planets: '惑星',
|
||||
details: '詳細',
|
||||
you: 'あなた',
|
||||
scoreBreakdown: 'スコア内訳',
|
||||
noData: 'ランキングデータがありません'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,13 @@ export default {
|
||||
requirementsNotMet: '요구사항 미충족',
|
||||
current: '현재',
|
||||
level: '레벨',
|
||||
to: '~',
|
||||
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.',
|
||||
view: '보기',
|
||||
viewDetails: '상세 보기',
|
||||
exitConfirmTitle: '게임 종료',
|
||||
exitConfirmMessage: '게임을 종료하시겠습니까? 진행 상황은 자동으로 저장됩니다.'
|
||||
exitConfirmMessage: '게임을 종료하시겠습니까? 진행 상황은 자동으로 저장됩니다.',
|
||||
points: '포인트'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '전제 조건 미충족',
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
galaxy: '은하계',
|
||||
diplomacy: '외교',
|
||||
achievements: '업적',
|
||||
ranking: '랭킹',
|
||||
messages: '메시지',
|
||||
settings: '설정',
|
||||
gm: 'GM'
|
||||
@@ -110,7 +114,8 @@ export default {
|
||||
perHour: '시간',
|
||||
perMinute: '분',
|
||||
hour: '시간',
|
||||
noEnergy: '에너지 부족'
|
||||
noEnergy: '에너지 부족',
|
||||
temperatureBonus: '온도 보너스'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: '에너지 부족! 자원 생산 중단!',
|
||||
@@ -120,6 +125,12 @@ export default {
|
||||
deficitDetail: '에너지 부족: {deficit}, 발전소를 더 건설하세요',
|
||||
buildSolarPlant: '발전소 건설'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: '광맥이 부족합니다!',
|
||||
depletedWarning: '광맥이 고갈되었습니다!',
|
||||
depletedResources: '고갈됨: {resources}',
|
||||
lowResources: '곧 고갈: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: '행성',
|
||||
moon: '위성',
|
||||
@@ -186,12 +197,20 @@ export default {
|
||||
researchSpeedBonus: '연구 속도 보너스',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
missileCapacity: 'Missile Capacity',
|
||||
|
||||
// 광맥 매장량
|
||||
oreDeposit: '광맥 매장량',
|
||||
remainingDeposit: '남은 양',
|
||||
depletionTime: '고갈 예상',
|
||||
depositDepleted: '고갈됨',
|
||||
depositWarning: '경고: 광맥 매장량이 거의 고갈되었습니다 (10% 미만)!',
|
||||
depositDepletedMessage: '광맥이 고갈되었습니다. 생산이 중단되었습니다.'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '금속 자원 채굴',
|
||||
crystalMine: '크리스탈 자원 채굴',
|
||||
deuteriumSynthesizer: '중수소 자원 합성',
|
||||
deuteriumSynthesizer: '중수소 자원 합성 (온도가 낮을수록 생산량 증가)',
|
||||
solarPlant: '에너지 제공',
|
||||
fusionReactor: '중수소를 사용하여 대량의 에너지 생산',
|
||||
roboticsFactory: '건설 속도 향상',
|
||||
@@ -241,7 +260,7 @@ export default {
|
||||
colonyShip: '새로운 행성 식민에 사용',
|
||||
recycler: '잔해장 자원 수집',
|
||||
espionageProbe: '적 행성 정찰',
|
||||
solarSatellite: '추가 에너지 제공, 위성당 50 에너지 생성',
|
||||
solarSatellite: '추가 에너지 제공, 행성 온도에 따라 생산량 변동 (온도가 높을수록 증가)',
|
||||
darkMatterHarvester: '암흑 물질 채취 전용 특수 함선',
|
||||
deathstar: '행성 전체를 파괴할 수 있는 궁극 병기'
|
||||
},
|
||||
@@ -366,8 +385,16 @@ export default {
|
||||
buildings: '건물',
|
||||
research: '연구',
|
||||
ships: '함선',
|
||||
defense: '방어'
|
||||
}
|
||||
defense: '방어',
|
||||
waiting: '대기'
|
||||
},
|
||||
waitingEmpty: '대기 중인 작업이 없습니다',
|
||||
addToWaiting: '대기열에 추가',
|
||||
remove: '제거',
|
||||
resourcesReady: '준비 완료',
|
||||
waitingResources: '대기 중',
|
||||
waitingQueueFull: '대기열이 가득 찼습니다',
|
||||
movedToQueue: '작업이 대기열로 이동되었습니다'
|
||||
},
|
||||
overview: {
|
||||
title: '행성 개요',
|
||||
@@ -380,7 +407,10 @@ export default {
|
||||
consumptionSourcesDesc: '건물 에너지 소비 세부 정보',
|
||||
totalProduction: '총 생산량',
|
||||
totalConsumption: '총 소비량',
|
||||
noConsumption: '에너지 소비 없음'
|
||||
noConsumption: '에너지 소비 없음',
|
||||
tabOverview: '개요',
|
||||
tabProduction: '생산 상세',
|
||||
tabConsumption: '소비 상세'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '건물',
|
||||
@@ -512,6 +542,29 @@ export default {
|
||||
spy: '정찰',
|
||||
deploy: '배치',
|
||||
expedition: '탐험',
|
||||
expeditionZone: '탐험 구역',
|
||||
expeditionZoneDesc: '목적지를 선택하세요. 구역마다 위험과 보상이 다릅니다',
|
||||
requiresAstro: '천체물리학 레벨 {level} 필요',
|
||||
reward: '보상',
|
||||
danger: '위험',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: '근우주',
|
||||
desc: '안전한 근우주, 위험은 낮지만 보상도 적음'
|
||||
},
|
||||
deepSpace: {
|
||||
name: '심우주',
|
||||
desc: '항성에서 멀리 떨어진 곳, 더 많은 자원을 발견할 수 있음'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: '미지의 우주',
|
||||
desc: '탐험되지 않은 구역, 고위험 고수익'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: '위험한 성운',
|
||||
desc: '알 수 없는 위험으로 가득한 성운, 하지만 매우 풍부한 보물을 품고 있음'
|
||||
}
|
||||
},
|
||||
recycle: '회수',
|
||||
transportResources: '자원 수송',
|
||||
totalCargoCapacity: '총 적재량',
|
||||
@@ -566,7 +619,24 @@ export default {
|
||||
presetName: '프리셋 이름',
|
||||
presetNamePlaceholder: '프리셋 이름 입력',
|
||||
deletePresetTitle: '프리셋 삭제',
|
||||
deletePresetMessage: '프리셋 "{name}"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'
|
||||
deletePresetMessage: '프리셋 "{name}"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
|
||||
// 점프 게이트
|
||||
jumpGate: '점프 게이트',
|
||||
jumpGateDescription: '점프 게이트를 사용하여 다른 점프 게이트가 있는 위성으로 함대를 즉시 전송',
|
||||
jumpGateNotAvailable: '점프 게이트 사용 불가',
|
||||
jumpGateRequiresMoon: '점프 게이트는 위성에서만 사용 가능',
|
||||
jumpGateNotBuilt: '현재 위성에 점프 게이트가 없습니다',
|
||||
jumpGateCooldown: '점프 게이트 쿨다운 중',
|
||||
jumpGateCooldownRemaining: '남은 쿨다운 시간',
|
||||
jumpGateReady: '점프 게이트 준비 완료',
|
||||
jumpGateSelectTarget: '목표 위성 선택',
|
||||
jumpGateNoTargetMoons: '사용 가능한 목표 위성 없음 (점프 게이트와 쿨다운 완료 필요)',
|
||||
jumpGateSelectFleet: '전송할 함대 선택',
|
||||
jumpGateTransfer: '함대 전송',
|
||||
jumpGateSuccess: '점프 게이트 전송 성공',
|
||||
jumpGateSuccessMessage: '함대가 {target}(으)로 즉시 전송되었습니다',
|
||||
jumpGateFailed: '점프 게이트 전송 실패',
|
||||
jumpGateFailedMessage: '점프 게이트 상태와 함대 구성을 확인하세요'
|
||||
},
|
||||
officersView: {
|
||||
title: '장교',
|
||||
@@ -625,6 +695,8 @@ export default {
|
||||
switch: '전환',
|
||||
recycle: '회수',
|
||||
debrisField: '잔해 필드',
|
||||
oreDeposits: '광맥 매장량',
|
||||
deposits: '매장량',
|
||||
scoutPlanetTitle: '행성 정찰',
|
||||
attackPlanetTitle: '행성 공격',
|
||||
missileAttackTitle: '미사일 공격',
|
||||
@@ -649,7 +721,9 @@ export default {
|
||||
giftPlanetTitle: '선물 보내기',
|
||||
giftPlanetMessage:
|
||||
'행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.',
|
||||
npcPlanetName: '{name}의 행성'
|
||||
npcPlanetName: '{name}의 행성',
|
||||
intercepted: '요격됨',
|
||||
defenseLosses: '방어 손실'
|
||||
},
|
||||
messagesView: {
|
||||
title: '메시지 센터',
|
||||
@@ -673,6 +747,8 @@ export default {
|
||||
attackerLosses: '공격자 손실',
|
||||
defenderLosses: '방어자 손실',
|
||||
noLosses: '손실 없음',
|
||||
losses: '손실',
|
||||
remainingUnits: '잔여 유닛',
|
||||
plunder: '약탈 자원',
|
||||
debrisField: '잔해장',
|
||||
resources: '자원',
|
||||
@@ -690,6 +766,18 @@ export default {
|
||||
round: '제{round}라운드',
|
||||
attackerRemainingPower: '공격자 잔여 화력',
|
||||
defenderRemainingPower: '방어자 잔여 화력',
|
||||
// 전투 애니메이션
|
||||
playAnimation: '애니메이션 재생',
|
||||
showDetails: '세부정보 표시',
|
||||
speed: '속도',
|
||||
power: '전투력',
|
||||
battleLogEmpty: '전투 기록이 비어 있습니다',
|
||||
roundStarted: '제{round}라운드 시작',
|
||||
shipDestroyed: '{ship} {count}대 파괴',
|
||||
defenseDestroyed: '{defense} {count}기 파괴',
|
||||
attackerWins: '공격자 승리',
|
||||
defenderWins: '방어자 승리',
|
||||
roundsPlayed: '라운드 완료',
|
||||
spied: '정찰당함',
|
||||
spiedNotification: '정찰 알림',
|
||||
noSpiedNotifications: '정찰 알림 없음',
|
||||
@@ -898,6 +986,7 @@ export default {
|
||||
inAppNotifications: '인앱 알림',
|
||||
constructionComplete: '건설 완료',
|
||||
researchComplete: '연구 완료',
|
||||
unlockNotification: '잠금 해제 알림',
|
||||
browserPermission: '브라우저 알림 활성화',
|
||||
permissionGranted: '권한 허용됨',
|
||||
permissionDenied: '권한 거부됨/허용되지 않음',
|
||||
@@ -905,11 +994,22 @@ export default {
|
||||
notificationsDisabled: '특정 알림을 설정하려면 위의 스위치 중 하나를 활성화하세요',
|
||||
suppressInFocus: '페이지가 포커스될 때 브라우저 알림 숨기기',
|
||||
expandTypes: '세부 정보 펼치기',
|
||||
collapseTypes: '세부 정보 접기'
|
||||
collapseTypes: '세부 정보 접기',
|
||||
// NPC 이름 업데이트
|
||||
npcNameUpdate: 'NPC 이름 업데이트',
|
||||
npcNameUpdateTitle: '이전 형식 NPC 이름 감지됨',
|
||||
npcNameUpdateMessage: '{count}개의 NPC가 이전 형식의 이름을 사용 중입니다. 새로운 현지화된 이름으로 업데이트하시겠습니까?',
|
||||
npcNameUpdateConfirm: '이름 업데이트',
|
||||
npcNameUpdateCancel: '현재 유지',
|
||||
npcNameUpdateSuccess: '{count}개의 NPC 이름이 업데이트되었습니다',
|
||||
npcNameUpdateSkipped: 'NPC 이름 업데이트 건너뜀'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: '건설 완료',
|
||||
researchComplete: '연구 완료'
|
||||
researchComplete: '연구 완료',
|
||||
newUnlock: '새 콘텐츠 잠금 해제',
|
||||
building: '건물',
|
||||
technology: '기술'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GM 제어판',
|
||||
@@ -1145,6 +1245,21 @@ export default {
|
||||
attackCooldown: '공격 쿨다운 중 ({min}분 {sec}초)',
|
||||
notSpiedYet: '아직 정찰하지 않음, 먼저 정찰 필요'
|
||||
}
|
||||
},
|
||||
aiType: 'AI 유형',
|
||||
aiTypes: {
|
||||
aggressive: '공격형',
|
||||
defensive: '방어형',
|
||||
trader: '상인형',
|
||||
expansionist: '확장형',
|
||||
balanced: '균형형'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: '적극적으로 정찰 및 공격, 강한 보복',
|
||||
defensive: '거의 공격하지 않음, 공격받으면 강하게 보복',
|
||||
trader: '거의 공격하지 않음, 거래와 선물 선호',
|
||||
expansionist: '발전에 집중, 공격성 낮음',
|
||||
balanced: '상황에 따라 전략을 동적으로 조정'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1296,7 +1411,8 @@ export default {
|
||||
},
|
||||
achievements: {
|
||||
title: '업적 시스템',
|
||||
message: '게임 목표를 완료하여 업적을 해제하고 암흑 물질 보상을 획득하세요! 업적은 여러 등급이 있으며, 더 높은 도전으로 더 좋은 보상을 받으세요.'
|
||||
message:
|
||||
'게임 목표를 완료하여 업적을 해제하고 암흑 물질 보상을 획득하세요! 업적은 여러 등급이 있으며, 더 높은 도전으로 더 좋은 보상을 받으세요.'
|
||||
},
|
||||
settings: {
|
||||
title: '설정',
|
||||
@@ -1403,5 +1519,24 @@ export default {
|
||||
robbed: 'NPC에게 잔해 회수당한 횟수',
|
||||
lostToNPC: 'NPC에게 빼앗긴 잔해 자원 총량'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: '랭킹',
|
||||
totalPlayers: '{count} 플레이어',
|
||||
yourRanking: '내 순위',
|
||||
categories: {
|
||||
total: '종합',
|
||||
building: '건물',
|
||||
research: '연구',
|
||||
fleet: '함대',
|
||||
defense: '방어'
|
||||
},
|
||||
points: '점',
|
||||
name: '이름',
|
||||
planets: '행성',
|
||||
details: '상세',
|
||||
you: '나',
|
||||
scoreBreakdown: '점수 상세',
|
||||
noData: '랭킹 데이터 없음'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,13 @@ export default {
|
||||
requirementsNotMet: 'Требования не выполнены',
|
||||
current: 'Текущий',
|
||||
level: 'Уровень',
|
||||
to: 'до',
|
||||
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.',
|
||||
view: 'Просмотр',
|
||||
viewDetails: 'Подробнее',
|
||||
exitConfirmTitle: 'Выйти из игры',
|
||||
exitConfirmMessage: 'Вы уверены, что хотите выйти? Прогресс сохраняется автоматически.'
|
||||
exitConfirmMessage: 'Вы уверены, что хотите выйти? Прогресс сохраняется автоматически.',
|
||||
points: 'Очки'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: 'Требования не выполнены',
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
galaxy: 'Галактика',
|
||||
diplomacy: 'Дипломатия',
|
||||
achievements: 'Достижения',
|
||||
ranking: 'Рейтинг',
|
||||
messages: 'Сообщения',
|
||||
settings: 'Настройки',
|
||||
gm: 'GM'
|
||||
@@ -110,7 +114,8 @@ export default {
|
||||
perHour: 'час',
|
||||
perMinute: 'мин',
|
||||
hour: 'час',
|
||||
noEnergy: 'Нет энергии'
|
||||
noEnergy: 'Нет энергии',
|
||||
temperatureBonus: 'Температурный бонус'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
|
||||
@@ -120,6 +125,12 @@ export default {
|
||||
deficitDetail: 'Дефицит энергии: {deficit}, постройте больше электростанций',
|
||||
buildSolarPlant: 'Построить электростанцию'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: 'Залежи руды заканчиваются!',
|
||||
depletedWarning: 'Залежи руды истощены!',
|
||||
depletedResources: 'Истощено: {resources}',
|
||||
lowResources: 'Почти истощено: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: 'Планета',
|
||||
moon: 'Луна',
|
||||
@@ -186,12 +197,20 @@ export default {
|
||||
researchSpeedBonus: 'Бонус скорости исследования',
|
||||
planetSpace: 'Planet Space',
|
||||
moonSpace: 'Moon Space',
|
||||
missileCapacity: 'Missile Capacity'
|
||||
missileCapacity: 'Missile Capacity',
|
||||
|
||||
// Ore deposits
|
||||
oreDeposit: 'Рудный запас',
|
||||
remainingDeposit: 'Осталось',
|
||||
depletionTime: 'Расч. истощение',
|
||||
depositDepleted: 'Исчерпано',
|
||||
depositWarning: 'Внимание: Рудные запасы заканчиваются (ниже 10%)!',
|
||||
depositDepletedMessage: 'Рудные запасы исчерпаны. Производство остановлено.'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: 'Добывает металлические ресурсы',
|
||||
crystalMine: 'Добывает кристаллические ресурсы',
|
||||
deuteriumSynthesizer: 'Синтезирует дейтериевые ресурсы',
|
||||
deuteriumSynthesizer: 'Синтезирует дейтерий (выше выход при низких температурах)',
|
||||
solarPlant: 'Обеспечивает энергией',
|
||||
fusionReactor: 'Использует дейтерий для производства большого количества энергии',
|
||||
roboticsFactory: 'Ускоряет скорость строительства',
|
||||
@@ -241,7 +260,7 @@ export default {
|
||||
colonyShip: 'Используется для колонизации новых планет',
|
||||
recycler: 'Собирает ресурсы с поля обломков',
|
||||
espionageProbe: 'Разведывает вражеские планеты',
|
||||
solarSatellite: 'Обеспечивает дополнительную энергию, генерирует 50 энергии на спутник',
|
||||
solarSatellite: 'Обеспечивает дополнительную энергию, выход зависит от температуры планеты (выше при жаре)',
|
||||
darkMatterHarvester: 'Специальный корабль для сбора тёмной материи',
|
||||
deathstar: 'Абсолютное оружие, способное уничтожать целые планеты'
|
||||
},
|
||||
@@ -367,8 +386,16 @@ export default {
|
||||
buildings: 'Здания',
|
||||
research: 'Исследования',
|
||||
ships: 'Корабли',
|
||||
defense: 'Оборона'
|
||||
}
|
||||
defense: 'Оборона',
|
||||
waiting: 'Ожидание'
|
||||
},
|
||||
waitingEmpty: 'Нет ожидающих задач',
|
||||
addToWaiting: 'Добавить в очередь ожидания',
|
||||
remove: 'Удалить',
|
||||
resourcesReady: 'Готово',
|
||||
waitingResources: 'Ожидание',
|
||||
waitingQueueFull: 'Очередь ожидания заполнена',
|
||||
movedToQueue: 'Задача перемещена в очередь'
|
||||
},
|
||||
overview: {
|
||||
title: 'Обзор планеты',
|
||||
@@ -381,7 +408,10 @@ export default {
|
||||
consumptionSourcesDesc: 'Детали потребления энергии зданиями',
|
||||
totalProduction: 'Общее производство',
|
||||
totalConsumption: 'Общее потребление',
|
||||
noConsumption: 'Нет потребления энергии'
|
||||
noConsumption: 'Нет потребления энергии',
|
||||
tabOverview: 'Обзор',
|
||||
tabProduction: 'Детали производства',
|
||||
tabConsumption: 'Детали потребления'
|
||||
},
|
||||
buildingsView: {
|
||||
title: 'Здания',
|
||||
@@ -515,6 +545,29 @@ export default {
|
||||
spy: 'Разведка',
|
||||
deploy: 'Размещение',
|
||||
expedition: 'Экспедиция',
|
||||
expeditionZone: 'Зона экспедиции',
|
||||
expeditionZoneDesc: 'Выберите пункт назначения. Разные зоны имеют разные риски и награды',
|
||||
requiresAstro: 'Требуется Астрофизика уровня {level}',
|
||||
reward: 'Награда',
|
||||
danger: 'Опасность',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: 'Ближний космос',
|
||||
desc: 'Безопасный ближний космос, низкий риск, но меньше наград'
|
||||
},
|
||||
deepSpace: {
|
||||
name: 'Глубокий космос',
|
||||
desc: 'Далеко от звёзд, можно найти больше ресурсов'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: 'Неизведанный космос',
|
||||
desc: 'Неисследованная область, высокий риск — высокая награда'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: 'Опасная туманность',
|
||||
desc: 'Туманность полная неизвестных опасностей, но содержит очень богатые сокровища'
|
||||
}
|
||||
},
|
||||
recycle: 'Переработка',
|
||||
transportResources: 'Транспортировка ресурсов',
|
||||
totalCargoCapacity: 'Общая грузоподъёмность',
|
||||
@@ -569,7 +622,24 @@ export default {
|
||||
presetName: 'Имя шаблона',
|
||||
presetNamePlaceholder: 'Введите имя шаблона',
|
||||
deletePresetTitle: 'Удалить шаблон',
|
||||
deletePresetMessage: 'Вы уверены, что хотите удалить шаблон "{name}"? Это действие нельзя отменить.'
|
||||
deletePresetMessage: 'Вы уверены, что хотите удалить шаблон "{name}"? Это действие нельзя отменить.',
|
||||
// Прыжковые ворота
|
||||
jumpGate: 'Прыжковые ворота',
|
||||
jumpGateDescription: 'Используйте прыжковые ворота для мгновенной переброски флота на другую луну с вратами',
|
||||
jumpGateNotAvailable: 'Прыжковые ворота недоступны',
|
||||
jumpGateRequiresMoon: 'Прыжковые ворота можно использовать только на лунах',
|
||||
jumpGateNotBuilt: 'На текущей луне нет прыжковых ворот',
|
||||
jumpGateCooldown: 'Прыжковые ворота перезаряжаются',
|
||||
jumpGateCooldownRemaining: 'Оставшееся время перезарядки',
|
||||
jumpGateReady: 'Прыжковые ворота готовы',
|
||||
jumpGateSelectTarget: 'Выберите целевую луну',
|
||||
jumpGateNoTargetMoons: 'Нет доступных целевых лун (требуются ворота и завершённая перезарядка)',
|
||||
jumpGateSelectFleet: 'Выберите флот для переброски',
|
||||
jumpGateTransfer: 'Перебросить флот',
|
||||
jumpGateSuccess: 'Переброска через ворота успешна',
|
||||
jumpGateSuccessMessage: 'Флот мгновенно переброшен к {target}',
|
||||
jumpGateFailed: 'Переброска через ворота не удалась',
|
||||
jumpGateFailedMessage: 'Проверьте состояние ворот и конфигурацию флота'
|
||||
},
|
||||
officersView: {
|
||||
title: 'Офицеры',
|
||||
@@ -628,6 +698,8 @@ export default {
|
||||
switch: 'Переключить',
|
||||
recycle: 'Переработка',
|
||||
debrisField: 'Поле обломков',
|
||||
oreDeposits: 'Рудные запасы',
|
||||
deposits: 'Запасы',
|
||||
scoutPlanetTitle: 'Разведать планету',
|
||||
attackPlanetTitle: 'Атаковать планету',
|
||||
missileAttackTitle: 'Ракетная атака',
|
||||
@@ -655,7 +727,9 @@ export default {
|
||||
giftPlanetTitle: 'Отправить подарок',
|
||||
giftPlanetMessage:
|
||||
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.',
|
||||
npcPlanetName: 'Планета {name}'
|
||||
npcPlanetName: 'Планета {name}',
|
||||
intercepted: 'Перехвачено',
|
||||
defenseLosses: 'Потери обороны'
|
||||
},
|
||||
messagesView: {
|
||||
title: 'Сообщения',
|
||||
@@ -679,6 +753,8 @@ export default {
|
||||
attackerLosses: 'Потери нападающего',
|
||||
defenderLosses: 'Потери защитника',
|
||||
noLosses: 'Без потерь',
|
||||
losses: 'Потери',
|
||||
remainingUnits: 'Оставшиеся юниты',
|
||||
plunder: 'Добыча',
|
||||
debrisField: 'Поле обломков',
|
||||
resources: 'Ресурсы',
|
||||
@@ -696,6 +772,18 @@ export default {
|
||||
round: 'Раунд {round}',
|
||||
attackerRemainingPower: 'Оставшаяся мощь нападающего',
|
||||
defenderRemainingPower: 'Оставшаяся мощь защитника',
|
||||
// Анимация битвы
|
||||
playAnimation: 'Воспроизвести анимацию',
|
||||
showDetails: 'Показать детали',
|
||||
speed: 'Скорость',
|
||||
power: 'Боевая мощь',
|
||||
battleLogEmpty: 'Журнал битвы пуст',
|
||||
roundStarted: 'Раунд {round} начался',
|
||||
shipDestroyed: '{count} {ship} уничтожено',
|
||||
defenseDestroyed: '{count} {defense} уничтожено',
|
||||
attackerWins: 'Победа нападающего',
|
||||
defenderWins: 'Победа защитника',
|
||||
roundsPlayed: 'раундов сыграно',
|
||||
spied: 'Шпионаж',
|
||||
spiedNotification: 'Уведомление о шпионаже',
|
||||
noSpiedNotifications: 'Нет уведомлений о шпионаже',
|
||||
@@ -775,7 +863,8 @@ export default {
|
||||
colonizeSuccess: 'Миссия колонизации успешна, новая планета создана',
|
||||
colonizeFailed: 'Миссия колонизации провалена',
|
||||
colonizeFailedOccupied: 'Колонизация провалена: Целевая позиция уже занята другой планетой',
|
||||
colonizeFailedMaxColonies: 'Колонизация провалена: Достигнуто максимальное количество колоний. Исследуйте Астрофизику для увеличения лимита.',
|
||||
colonizeFailedMaxColonies:
|
||||
'Колонизация провалена: Достигнуто максимальное количество колоний. Исследуйте Астрофизику для увеличения лимита.',
|
||||
spySuccess: 'Миссия шпионажа успешно завершена',
|
||||
spyFailed: 'Миссия шпионажа провалена',
|
||||
spyFailedTargetNotFound: 'Шпионаж провален: Целевая планета не существует',
|
||||
@@ -904,6 +993,7 @@ export default {
|
||||
inAppNotifications: 'Внутриигровые уведомления',
|
||||
constructionComplete: 'Строительство завершено',
|
||||
researchComplete: 'Исследование завершено',
|
||||
unlockNotification: 'Уведомление о разблокировке',
|
||||
browserPermission: 'Включить уведомления браузера',
|
||||
permissionGranted: 'Разрешение получено',
|
||||
permissionDenied: 'Разрешение отклонено/не получено',
|
||||
@@ -911,11 +1001,22 @@ export default {
|
||||
notificationsDisabled: 'Включите любой переключатель выше для настройки конкретных уведомлений',
|
||||
suppressInFocus: 'Не отправлять уведомления браузера, когда страница в фокусе',
|
||||
expandTypes: 'Развернуть детали',
|
||||
collapseTypes: 'Свернуть детали'
|
||||
collapseTypes: 'Свернуть детали',
|
||||
// Обновление имен NPC
|
||||
npcNameUpdate: 'Обновление имен NPC',
|
||||
npcNameUpdateTitle: 'Обнаружены устаревшие имена NPC',
|
||||
npcNameUpdateMessage: 'Найдено {count} NPC с устаревшим форматом имен. Хотите обновить их до новых локализованных имен?',
|
||||
npcNameUpdateConfirm: 'Обновить имена',
|
||||
npcNameUpdateCancel: 'Оставить как есть',
|
||||
npcNameUpdateSuccess: 'Успешно обновлено {count} имен NPC',
|
||||
npcNameUpdateSkipped: 'Обновление имен NPC пропущено'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: 'Строительство завершено',
|
||||
researchComplete: 'Исследование завершено'
|
||||
researchComplete: 'Исследование завершено',
|
||||
newUnlock: 'Разблокирован новый контент',
|
||||
building: 'Здание',
|
||||
technology: 'Технология'
|
||||
},
|
||||
gmView: {
|
||||
title: 'Панель управления GM',
|
||||
@@ -1151,6 +1252,21 @@ export default {
|
||||
attackCooldown: 'Атака на перезарядке ({min}м {sec}с)',
|
||||
notSpiedYet: 'Ещё не разведан, сначала нужна разведка'
|
||||
}
|
||||
},
|
||||
aiType: 'Тип ИИ',
|
||||
aiTypes: {
|
||||
aggressive: 'Агрессивный',
|
||||
defensive: 'Оборонительный',
|
||||
trader: 'Торговец',
|
||||
expansionist: 'Экспансионист',
|
||||
balanced: 'Сбалансированный'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: 'Активно шпионит и атакует, сильная месть',
|
||||
defensive: 'Редко атакует, сильная месть при нападении',
|
||||
trader: 'Почти не атакует, предпочитает торговлю и подарки',
|
||||
expansionist: 'Фокус на развитии, менее агрессивен',
|
||||
balanced: 'Динамически адаптирует стратегию к ситуации'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1310,7 +1426,8 @@ export default {
|
||||
},
|
||||
achievements: {
|
||||
title: 'Система достижений',
|
||||
message: 'Выполняйте игровые цели для разблокировки достижений и получения наград в виде тёмной материи! Достижения имеют несколько уровней - стремитесь к более высоким целям для лучших наград.'
|
||||
message:
|
||||
'Выполняйте игровые цели для разблокировки достижений и получения наград в виде тёмной материи! Достижения имеют несколько уровней - стремитесь к более высоким целям для лучших наград.'
|
||||
},
|
||||
settings: {
|
||||
title: 'Настройки',
|
||||
@@ -1418,5 +1535,24 @@ export default {
|
||||
robbed: 'Раз НПС собрал обломки',
|
||||
lostToNPC: 'Всего обломков потеряно НПС'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: 'Рейтинг',
|
||||
totalPlayers: '{count} игроков',
|
||||
yourRanking: 'Ваш рейтинг',
|
||||
categories: {
|
||||
total: 'Всего',
|
||||
building: 'Здания',
|
||||
research: 'Исследования',
|
||||
fleet: 'Флот',
|
||||
defense: 'Оборона'
|
||||
},
|
||||
points: 'очк',
|
||||
name: 'Имя',
|
||||
planets: 'Планеты',
|
||||
details: 'Детали',
|
||||
you: 'Вы',
|
||||
scoreBreakdown: 'Детали очков',
|
||||
noData: 'Нет данных рейтинга'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,13 @@ export default {
|
||||
requirementsNotMet: '前置条件未满足',
|
||||
current: '当前',
|
||||
level: '等级',
|
||||
to: '至',
|
||||
gmModeActivated: 'GM 模式已激活!请查看导航菜单。',
|
||||
view: '查看',
|
||||
viewDetails: '查看详情',
|
||||
exitConfirmTitle: '退出游戏',
|
||||
exitConfirmMessage: '确定要退出游戏吗?游戏进度会自动保存。'
|
||||
exitConfirmMessage: '确定要退出游戏吗?游戏进度会自动保存。',
|
||||
points: '积分'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '不满足前置条件',
|
||||
@@ -84,6 +87,8 @@ export default {
|
||||
galaxy: '星系',
|
||||
diplomacy: '外交',
|
||||
achievements: '成就',
|
||||
campaign: '战役',
|
||||
ranking: '排行',
|
||||
messages: '消息',
|
||||
settings: '设置',
|
||||
gm: 'GM'
|
||||
@@ -109,7 +114,8 @@ export default {
|
||||
perHour: '小时',
|
||||
perMinute: '分钟',
|
||||
hour: '小时',
|
||||
noEnergy: '电力不足'
|
||||
noEnergy: '电力不足',
|
||||
temperatureBonus: '温度加成'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: '电力不足,资源生产已停止!',
|
||||
@@ -119,6 +125,12 @@ export default {
|
||||
deficitDetail: '电力缺口: {deficit},请建造更多电站',
|
||||
buildSolarPlant: '建造电站'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: '矿脉储量不足!',
|
||||
depletedWarning: '矿脉已耗尽!',
|
||||
depletedResources: '已耗尽: {resources}',
|
||||
lowResources: '即将耗尽: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: '星球',
|
||||
moon: '月球',
|
||||
@@ -182,12 +194,20 @@ export default {
|
||||
buildSpeedBonus: '建造速度加成',
|
||||
researchSpeedBonus: '研究速度加成',
|
||||
|
||||
missileCapacity: '导弹容量'
|
||||
missileCapacity: '导弹容量',
|
||||
|
||||
// 矿脉储量
|
||||
oreDeposit: '矿脉储量',
|
||||
remainingDeposit: '剩余储量',
|
||||
depletionTime: '预计耗尽',
|
||||
depositDepleted: '已耗尽',
|
||||
depositWarning: '警告:矿脉储量即将耗尽(低于10%)!',
|
||||
depositDepletedMessage: '矿脉已耗尽,产量已停止。'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '开采金属资源',
|
||||
crystalMine: '开采晶体资源',
|
||||
deuteriumSynthesizer: '合成重氢资源',
|
||||
deuteriumSynthesizer: '合成重氢资源(温度越低产量越高)',
|
||||
solarPlant: '提供能源',
|
||||
fusionReactor: '使用重氢产生大量能源',
|
||||
roboticsFactory: '加快建造速度',
|
||||
@@ -237,7 +257,7 @@ export default {
|
||||
colonyShip: '用于殖民新星球',
|
||||
recycler: '收集残骸场资源',
|
||||
espionageProbe: '侦察敌方星球',
|
||||
solarSatellite: '提供额外能源,每个产生50点电力',
|
||||
solarSatellite: '提供额外能源,产能受星球温度影响(温度越高产能越高)',
|
||||
darkMatterHarvester: '专门用于采集暗物质的特殊飞船',
|
||||
deathstar: '终极武器,能够摧毁整个行星'
|
||||
},
|
||||
@@ -365,8 +385,16 @@ export default {
|
||||
buildings: '建筑',
|
||||
research: '研究',
|
||||
ships: '舰船',
|
||||
defense: '防御'
|
||||
}
|
||||
defense: '防御',
|
||||
waiting: '等待'
|
||||
},
|
||||
waitingEmpty: '当前没有等待中的任务',
|
||||
addToWaiting: '添加到等待队列',
|
||||
remove: '移除',
|
||||
resourcesReady: '资源就绪',
|
||||
waitingResources: '等待资源',
|
||||
waitingQueueFull: '等待队列已满',
|
||||
movedToQueue: '任务已移至正式队列'
|
||||
},
|
||||
overview: {
|
||||
title: '星球总览',
|
||||
@@ -379,7 +407,10 @@ export default {
|
||||
consumptionSourcesDesc: '各建筑的电力消耗详情',
|
||||
totalProduction: '总产量',
|
||||
totalConsumption: '总消耗',
|
||||
noConsumption: '当前无电力消耗'
|
||||
noConsumption: '当前无电力消耗',
|
||||
tabOverview: '概览',
|
||||
tabProduction: '产量详情',
|
||||
tabConsumption: '消耗详情'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '建筑',
|
||||
@@ -503,6 +534,29 @@ export default {
|
||||
spy: '侦察',
|
||||
deploy: '部署',
|
||||
expedition: '探险',
|
||||
expeditionZone: '探险区域',
|
||||
expeditionZoneDesc: '选择探险目的地,不同区域有不同的风险和收益',
|
||||
requiresAstro: '需要天体物理学 {level} 级',
|
||||
reward: '收益',
|
||||
danger: '危险',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: '近空区域',
|
||||
desc: '安全的近地空间,风险低但收益也较少'
|
||||
},
|
||||
deepSpace: {
|
||||
name: '深空区域',
|
||||
desc: '远离恒星的深空,可能发现更多资源'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: '未知空间',
|
||||
desc: '未经探索的区域,高风险高回报'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: '危险星云',
|
||||
desc: '充满未知危险的星云,但蕴含极其丰富的宝藏'
|
||||
}
|
||||
},
|
||||
recycle: '回收',
|
||||
destroy: '行星毁灭',
|
||||
transportResources: '运输资源',
|
||||
@@ -636,6 +690,8 @@ export default {
|
||||
sendGift: '赠送礼物',
|
||||
debris: '残骸',
|
||||
debrisField: '残骸场',
|
||||
oreDeposits: '矿脉储量',
|
||||
deposits: '储量',
|
||||
scoutPlanetTitle: '侦察星球',
|
||||
attackPlanetTitle: '攻击星球',
|
||||
missileAttackTitle: '导弹攻击',
|
||||
@@ -676,7 +732,9 @@ export default {
|
||||
phalanxStatus: '状态',
|
||||
phalanxStatusOutbound: '前往中',
|
||||
phalanxStatusReturning: '返回中',
|
||||
phalanxInsufficientDeuterium: '氘不足'
|
||||
phalanxInsufficientDeuterium: '氘不足',
|
||||
intercepted: '被拦截',
|
||||
defenseLosses: '防御损失'
|
||||
},
|
||||
messagesView: {
|
||||
title: '消息中心',
|
||||
@@ -700,6 +758,8 @@ export default {
|
||||
attackerLosses: '攻击方损失',
|
||||
defenderLosses: '防守方损失',
|
||||
noLosses: '无损失',
|
||||
losses: '损失统计',
|
||||
remainingUnits: '剩余单位',
|
||||
plunder: '掠夺资源',
|
||||
debrisField: '残骸场',
|
||||
resources: '资源',
|
||||
@@ -717,6 +777,18 @@ export default {
|
||||
round: '第{round}回合',
|
||||
attackerRemainingPower: '攻击方剩余火力',
|
||||
defenderRemainingPower: '防守方剩余火力',
|
||||
// 战斗动画相关
|
||||
playAnimation: '播放动画',
|
||||
showDetails: '显示详情',
|
||||
speed: '速度',
|
||||
power: '战斗力',
|
||||
battleLogEmpty: '战斗日志为空',
|
||||
roundStarted: '第{round}回合开始',
|
||||
shipDestroyed: '{count}艘{ship}被摧毁',
|
||||
defenseDestroyed: '{count}座{defense}被摧毁',
|
||||
attackerWins: '攻击方获胜',
|
||||
defenderWins: '防守方获胜',
|
||||
roundsPlayed: '回合已播放',
|
||||
spied: '被侦查',
|
||||
spiedNotification: '被侦查通知',
|
||||
noSpiedNotifications: '暂无被侦查通知',
|
||||
@@ -930,6 +1002,7 @@ export default {
|
||||
inAppNotifications: '页面内通知',
|
||||
constructionComplete: '建筑完成',
|
||||
researchComplete: '研究完成',
|
||||
unlockNotification: '解锁通知',
|
||||
browserPermission: '启用浏览器通知',
|
||||
permissionGranted: '已获得权限',
|
||||
permissionDenied: '权限被拒绝/未获得',
|
||||
@@ -937,11 +1010,22 @@ export default {
|
||||
notificationsDisabled: '启用上方任一开关以配置具体通知',
|
||||
suppressInFocus: '页面聚焦时不发送浏览器通知',
|
||||
expandTypes: '展开详细设置',
|
||||
collapseTypes: '收起详细设置'
|
||||
collapseTypes: '收起详细设置',
|
||||
// NPC名称更新
|
||||
npcNameUpdate: 'NPC名称更新',
|
||||
npcNameUpdateTitle: '检测到旧版NPC名称',
|
||||
npcNameUpdateMessage: '发现 {count} 个NPC使用旧版名称格式。是否更新为新的本地化名称?',
|
||||
npcNameUpdateConfirm: '更新名称',
|
||||
npcNameUpdateCancel: '保持原样',
|
||||
npcNameUpdateSuccess: '已成功更新 {count} 个NPC名称',
|
||||
npcNameUpdateSkipped: '已跳过NPC名称更新'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: '建造完成',
|
||||
researchComplete: '研究完成'
|
||||
researchComplete: '研究完成',
|
||||
newUnlock: '新内容解锁',
|
||||
building: '建筑',
|
||||
technology: '科技'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GM 管理面板',
|
||||
@@ -1151,6 +1235,21 @@ export default {
|
||||
medium: '普通',
|
||||
hard: '困难'
|
||||
},
|
||||
aiType: 'AI类型',
|
||||
aiTypes: {
|
||||
aggressive: '侵略型',
|
||||
defensive: '防守型',
|
||||
trader: '商人型',
|
||||
expansionist: '扩张型',
|
||||
balanced: '平衡型'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: '积极侦查和攻击,反击猛烈',
|
||||
defensive: '很少主动攻击,被攻击后强烈反击',
|
||||
trader: '几乎不攻击,更愿意交易和送礼',
|
||||
expansionist: '专注发展,较少攻击',
|
||||
balanced: '根据情况动态调整策略'
|
||||
},
|
||||
reputation: '好感度',
|
||||
spyProbes: '侦察机数量',
|
||||
fleetPower: '舰队战力',
|
||||
@@ -1537,5 +1636,339 @@ export default {
|
||||
robbed: '被NPC回收残骸次数',
|
||||
lostToNPC: '被NPC回收的残骸资源总量'
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: '排行榜',
|
||||
totalPlayers: '共 {count} 名玩家',
|
||||
yourRanking: '你的排名',
|
||||
categories: {
|
||||
total: '总积分',
|
||||
building: '建筑',
|
||||
research: '研究',
|
||||
fleet: '舰队',
|
||||
defense: '防御'
|
||||
},
|
||||
points: '分',
|
||||
name: '名称',
|
||||
planets: '星球',
|
||||
details: '详情',
|
||||
you: '你',
|
||||
scoreBreakdown: '积分详情',
|
||||
noData: '暂无排行数据'
|
||||
},
|
||||
campaign: {
|
||||
name: '战役',
|
||||
description: '探索神秘的银河系,揭开古代文明的秘密',
|
||||
totalProgress: '总进度',
|
||||
questsCompleted: '任务完成',
|
||||
chapter: '章节',
|
||||
branch: '分支',
|
||||
startQuest: '开始任务',
|
||||
claimRewards: '领取奖励',
|
||||
objectives: '任务目标',
|
||||
objectivesLabel: '目标',
|
||||
rewards: '任务奖励',
|
||||
completed: '已完成',
|
||||
inProgress: '进行中',
|
||||
available: '可接取',
|
||||
locked: '未解锁',
|
||||
notifications: {
|
||||
questStarted: '任务已开始',
|
||||
questCompleted: '任务完成!',
|
||||
rewardsClaimed: '奖励已领取',
|
||||
objectiveCompleted: '目标完成',
|
||||
chapterUnlocked: '新章节已解锁'
|
||||
},
|
||||
dialogue: {
|
||||
skip: '跳过',
|
||||
continue: '继续',
|
||||
finish: '完成',
|
||||
player: '指挥官',
|
||||
npc: 'NPC',
|
||||
narrator: '旁白',
|
||||
mysterious: '神秘信号',
|
||||
unknownSource: '信号来源不明'
|
||||
},
|
||||
chapters: {
|
||||
'1': {
|
||||
title: '起源之地',
|
||||
description: '建设你的家园,迈出星际征途的第一步',
|
||||
backgroundStory: '你是一位年轻的星际指挥官,刚刚获得了属于自己的第一颗星球。在这片陌生的宇宙中,你将建设家园,发展科技,探索未知的银河系深处...'
|
||||
},
|
||||
'2': {
|
||||
title: '星际探索',
|
||||
description: '探索宇宙,发现神秘的古代遗迹',
|
||||
backgroundStory: '随着你的势力不断壮大,来自深空的神秘信号引起了你的注意。这些信号似乎指向一个古老的秘密,等待着勇敢的探索者去揭开...'
|
||||
},
|
||||
'3': {
|
||||
title: '银河外交',
|
||||
description: '与其他势力建立联系,在星际政治中立足',
|
||||
backgroundStory: '银河系中并不只有你一方势力。其他文明正在崛起,你需要决定是与他们为敌还是结盟。外交的智慧将决定你的帝国能走多远...'
|
||||
},
|
||||
'4': {
|
||||
title: '暗影降临',
|
||||
description: '面对强大的敌人,保卫你的领地',
|
||||
backgroundStory: '暗影中潜伏着危险。一个强大的敌对势力已经盯上了你的领地。战争不可避免,你必须做好准备,迎接即将到来的风暴...'
|
||||
},
|
||||
'5': {
|
||||
title: '古代秘密',
|
||||
description: '揭开银河系最深处的秘密',
|
||||
backgroundStory: '所有的线索都指向银河系最神秘的区域。在那里,隐藏着古代文明留下的终极秘密。你准备好揭开这一切了吗?'
|
||||
}
|
||||
},
|
||||
quests: {
|
||||
'1_1': {
|
||||
title: '家园建设',
|
||||
description: '建造基础设施,为你的星球奠定发展基础'
|
||||
},
|
||||
'1_2': {
|
||||
title: '科技启蒙',
|
||||
description: '研究基础科技,开启科技发展之路'
|
||||
},
|
||||
'1_3': {
|
||||
title: '第一艘船',
|
||||
description: '建造你的第一艘战舰'
|
||||
},
|
||||
'1_4': {
|
||||
title: '陌生邻居',
|
||||
description: '侦查附近星系中的其他势力'
|
||||
},
|
||||
'1_5': {
|
||||
title: '初次接触',
|
||||
description: '与附近的NPC势力建立初步联系'
|
||||
},
|
||||
'2_1': {
|
||||
title: '殖民先驱',
|
||||
description: '殖民你的第一颗新星球'
|
||||
},
|
||||
'2_2': {
|
||||
title: '深空探险',
|
||||
description: '派遣舰队进行远征探险'
|
||||
},
|
||||
'2_3': {
|
||||
title: '神秘信号',
|
||||
description: '调查来自深空的神秘信号'
|
||||
},
|
||||
'2_4': {
|
||||
title: '遗迹调查',
|
||||
description: '探索发现的古代遗迹'
|
||||
},
|
||||
'2_5': {
|
||||
title: '解密档案',
|
||||
description: '研究从遗迹中获得的数据'
|
||||
},
|
||||
'3_1': {
|
||||
title: '和平使者',
|
||||
description: '通过外交手段提升与NPC的关系'
|
||||
},
|
||||
'3_2': {
|
||||
title: '利益交换',
|
||||
description: '与友好势力建立稳定的关系'
|
||||
},
|
||||
'3_3': {
|
||||
title: '共同威胁',
|
||||
description: '发现潜在的敌对势力'
|
||||
},
|
||||
'3_4': {
|
||||
title: '联盟谈判',
|
||||
description: '与友好NPC建立正式同盟'
|
||||
},
|
||||
'3_5': {
|
||||
title: '备战风暴',
|
||||
description: '建设防御设施,准备迎接挑战'
|
||||
},
|
||||
'4_1': {
|
||||
title: '前哨遭袭',
|
||||
description: '抵御敌对势力的首次进攻'
|
||||
},
|
||||
'4_2': {
|
||||
title: '情报收集',
|
||||
description: '侦查敌方的军事部署'
|
||||
},
|
||||
'4_3': {
|
||||
title: '反击行动',
|
||||
description: '对敌方发起反击'
|
||||
},
|
||||
'4_4': {
|
||||
title: '资源争夺',
|
||||
description: '回收战场残骸,获取资源'
|
||||
},
|
||||
'4_5': {
|
||||
title: '决战前夕',
|
||||
description: '建造强大的舰队,准备最终决战'
|
||||
},
|
||||
'5_1': {
|
||||
title: '遗迹深处',
|
||||
description: '探索遗迹的最深处'
|
||||
},
|
||||
'5_2': {
|
||||
title: '古代科技',
|
||||
description: '解锁古代文明的科技'
|
||||
},
|
||||
'5_3': {
|
||||
title: '最终对决',
|
||||
description: '与神秘敌人进行最终决战'
|
||||
},
|
||||
'5_4': {
|
||||
title: '新纪元',
|
||||
description: '建立新的殖民地,开启新时代'
|
||||
},
|
||||
'5_5': {
|
||||
title: '传承延续',
|
||||
description: '继续发展,征服更多的星系'
|
||||
}
|
||||
},
|
||||
objectiveTypes: {
|
||||
buildBuilding: '建造 {building} 到 {level} 级',
|
||||
researchTech: '研究 {tech} 到 {level} 级',
|
||||
produceShips: '生产 {count} 艘 {ship}',
|
||||
accumulateResources: '积累 {amount} {resource}',
|
||||
defeatNPC: '击败 {npc}',
|
||||
winBattles: '赢得 {count} 场战斗',
|
||||
recycleDebris: '回收 {amount} 残骸',
|
||||
reachRelation: '与 {npc} 达到 {level} 关系',
|
||||
sendGift: '向 {npc} 送礼 {count} 次',
|
||||
formAlliance: '与 {npc} 结盟',
|
||||
colonize: '殖民 {count} 颗星球',
|
||||
expedition: '完成 {count} 次探险',
|
||||
spyTarget: '侦查 {target}'
|
||||
},
|
||||
errors: {
|
||||
questNotFound: '任务不存在',
|
||||
questNotAvailable: '任务不可接取',
|
||||
questNotActive: '任务未激活',
|
||||
questNotCompleted: '任务未完成',
|
||||
rewardsAlreadyClaimed: '奖励已领取',
|
||||
prerequisiteNotMet: '前置任务未完成'
|
||||
},
|
||||
speakers: {
|
||||
ancientVoice: '古代之声',
|
||||
neighborNPC: '邻近势力',
|
||||
mysteriousSignal: '神秘信号',
|
||||
enemyCommander: '敌方指挥官'
|
||||
},
|
||||
objectiveDescriptions: {
|
||||
buildMetalMine: '建造金属矿到2级',
|
||||
buildCrystalMine: '建造晶体矿到2级',
|
||||
buildSolarPlant: '建造太阳能电站到2级',
|
||||
buildResearchLab: '建造研究实验室到1级',
|
||||
researchEnergy: '研究能量技术到1级',
|
||||
buildShipyard: '建造船坞到2级',
|
||||
researchCombustion: '研究燃烧驱动到1级',
|
||||
buildLightFighters: '建造5艘轻型战斗机',
|
||||
researchEspionage: '研究间谍技术到2级',
|
||||
buildSpyProbes: '建造3艘间谍探测器',
|
||||
spyAnyNPC: '侦查任意NPC星球',
|
||||
sendGiftToNPC: '向任意NPC送礼',
|
||||
researchAstrophysics: '研究天体物理学到1级',
|
||||
buildColonyShip: '建造殖民船',
|
||||
colonizeNewPlanet: '殖民新星球',
|
||||
completeExpedition: '完成远征任务',
|
||||
discoverRuins: '发现古代遗迹',
|
||||
researchComputer: '研究电脑技术到4级',
|
||||
improveRelation: '提升与NPC的关系',
|
||||
reachFriendly: '与NPC达到友好关系',
|
||||
spyHostileNPC: '侦查敌对NPC',
|
||||
formAlliance: '与友好NPC结盟',
|
||||
buildDefenses: '建造防御设施',
|
||||
winDefenseBattle: '赢得防御战斗',
|
||||
spyEnemyPlanet: '侦查敌方星球',
|
||||
attackEnemy: '攻击敌方',
|
||||
recycleDebris: '回收残骸',
|
||||
buildBattleships: '建造10艘战列舰',
|
||||
exploreDeepRuins: '探索遗迹深处',
|
||||
researchHyperspace: '研究超空间技术',
|
||||
defeatBoss: '击败神秘敌人',
|
||||
colonizeSpecial: '殖民特殊位置',
|
||||
continueDevelopment: '继续发展'
|
||||
},
|
||||
dialogues: {
|
||||
'1_1': {
|
||||
prologue_1: '欢迎来到银河系,年轻的指挥官。这片广袤的宇宙等待着你的探索。首先,让我们建设好你的家园星球。',
|
||||
prologue_2: '我感应到了新的意识觉醒...有趣...让我们看看你能走多远...'
|
||||
},
|
||||
'1_2': {
|
||||
prologue_1: '基础设施已经建设完成。现在,是时候发展科技了。建造研究实验室,开启你的科技之路。'
|
||||
},
|
||||
'1_3': {
|
||||
prologue_1: '有了科技的支持,你可以开始建造舰队了。建造船坞,生产你的第一艘战舰。'
|
||||
},
|
||||
'1_4': {
|
||||
prologue_1: '你的舰队已经成型。现在,让我们了解一下周围的环境。派出间谍探测器,侦查附近的势力。',
|
||||
prologue_2: '你并不孤单...银河系中还有其他文明存在...'
|
||||
},
|
||||
'1_5': {
|
||||
prologue_1: '你已经发现了邻近的势力。外交是一门艺术,尝试与他们建立联系。',
|
||||
epilogue_1: '感谢你的礼物,指挥官。希望我们能成为朋友。',
|
||||
epilogue_2: '很好...建立联系是探索更深秘密的第一步...'
|
||||
},
|
||||
'2_1': {
|
||||
prologue_1: '你的势力已经稳固。是时候扩张领地了。研究天体物理学,建造殖民船,探索新的星球。',
|
||||
prologue_2: '宇宙是无限的...更多的星球意味着更多的可能性...'
|
||||
},
|
||||
'2_2': {
|
||||
prologue_1: '殖民成功!但宇宙深处还有更多秘密等待发现。派遣舰队进行远征探险吧。',
|
||||
prologue_2: '远方传来微弱的信号...那里有什么在等待着你...'
|
||||
},
|
||||
'2_3': {
|
||||
prologue_1: '你的探险队发现了异常信号。这些信号似乎来自一个古老的文明...调查它们的来源。',
|
||||
epilogue_1: '这些符号...是古代文明的遗迹!继续调查,揭开它们的秘密。'
|
||||
},
|
||||
'2_4': {
|
||||
prologue_1: '你发现了古代遗迹的位置。派遣舰队前去探索,看看能发现什么。'
|
||||
},
|
||||
'2_5': {
|
||||
prologue_1: '遗迹中发现了大量数据档案。研究这些数据,也许能解锁新的科技。'
|
||||
},
|
||||
'3_1': {
|
||||
prologue_1: '在探索的同时,也不要忘记外交。与周围的势力保持良好关系对你有益。'
|
||||
},
|
||||
'3_2': {
|
||||
prologue_1: '有些势力对你表示了友好。继续加深关系,也许能获得更多支持。'
|
||||
},
|
||||
'3_3': {
|
||||
prologue_1: '情报显示,有敌对势力正在暗中观察你。保持警惕,侦查他们的动向。'
|
||||
},
|
||||
'3_4': {
|
||||
prologue_1: '与友好势力建立正式同盟,在面对威胁时互相支持。'
|
||||
},
|
||||
'3_5': {
|
||||
prologue_1: '威胁正在逼近。建设防御设施,准备迎接可能的冲突。'
|
||||
},
|
||||
'4_1': {
|
||||
prologue_1: '敌人发动了进攻!保卫你的星球!',
|
||||
epilogue_1: '你成功击退了敌人的第一波进攻。但这只是开始...'
|
||||
},
|
||||
'4_2': {
|
||||
prologue_1: '敌人退却了,但他们还会回来。侦查他们的星球,了解他们的实力。'
|
||||
},
|
||||
'4_3': {
|
||||
prologue_1: '是时候反击了。进攻敌人的星球,削弱他们的力量。'
|
||||
},
|
||||
'4_4': {
|
||||
prologue_1: '战场上留下了大量残骸。回收这些资源,为下一场战斗做准备。'
|
||||
},
|
||||
'4_5': {
|
||||
prologue_1: '最终决战即将来临。建造强大的舰队,准备迎接最后的挑战。'
|
||||
},
|
||||
'5_1': {
|
||||
prologue_1: '所有线索都指向遗迹的最深处。那里隐藏着古代文明最核心的秘密。',
|
||||
prologue_2: '你终于来到了这里...真相即将揭晓...'
|
||||
},
|
||||
'5_2': {
|
||||
prologue_1: '在遗迹深处,你发现了失落的古代科技。研究并解锁它们的力量。'
|
||||
},
|
||||
'5_3': {
|
||||
prologue_1: '一个神秘的敌人出现了。这是最后的挑战,击败它!',
|
||||
epilogue_1: '你做到了!古代的守护者已被击败。银河系的秘密向你敞开。'
|
||||
},
|
||||
'5_4': {
|
||||
prologue_1: '和平终于来临。在这个新纪元中,建立新的殖民地,扩展你的帝国。'
|
||||
},
|
||||
'5_5': {
|
||||
prologue_1: '你的传奇才刚刚开始。继续探索,征服更多的星系!',
|
||||
epilogue_1: '银河系广阔无垠,还有无数秘密等待你去发现...'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,13 @@ export default {
|
||||
requirementsNotMet: '前置條件未滿足',
|
||||
current: '當前',
|
||||
level: '等級',
|
||||
to: '至',
|
||||
gmModeActivated: 'GM 模式已啟用!請查看導航選單。',
|
||||
view: '查看',
|
||||
viewDetails: '查看詳情',
|
||||
exitConfirmTitle: '退出遊戲',
|
||||
exitConfirmMessage: '確定要退出遊戲嗎?遊戲進度會自動儲存。'
|
||||
exitConfirmMessage: '確定要退出遊戲嗎?遊戲進度會自動儲存。',
|
||||
points: '積分'
|
||||
},
|
||||
errors: {
|
||||
requirementsNotMet: '不滿足前置條件',
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
galaxy: '星系',
|
||||
diplomacy: '外交',
|
||||
achievements: '成就',
|
||||
ranking: '排行',
|
||||
messages: '訊息',
|
||||
settings: '設定',
|
||||
guide: '遊戲指南',
|
||||
@@ -111,7 +115,8 @@ export default {
|
||||
perHour: '小時',
|
||||
perMinute: '分鐘',
|
||||
hour: '小時',
|
||||
noEnergy: '電力不足'
|
||||
noEnergy: '電力不足',
|
||||
temperatureBonus: '溫度加成'
|
||||
},
|
||||
energy: {
|
||||
lowWarning: '電力不足,資源生產已停止!',
|
||||
@@ -121,6 +126,12 @@ export default {
|
||||
deficitDetail: '電力缺口: {deficit},請建造更多電站',
|
||||
buildSolarPlant: '建造電站'
|
||||
},
|
||||
oreDeposit: {
|
||||
lowWarning: '礦脈儲量不足!',
|
||||
depletedWarning: '礦脈已耗盡!',
|
||||
depletedResources: '已耗盡: {resources}',
|
||||
lowResources: '即將耗盡: {resources}'
|
||||
},
|
||||
planet: {
|
||||
planet: '星球',
|
||||
moon: '月球',
|
||||
@@ -187,12 +198,20 @@ export default {
|
||||
researchSpeedBonus: '研究速度加成',
|
||||
planetSpace: '行星空間',
|
||||
moonSpace: '月球空間',
|
||||
missileCapacity: '導彈容量'
|
||||
missileCapacity: '導彈容量',
|
||||
|
||||
// 礦脈儲量
|
||||
oreDeposit: '礦脈儲量',
|
||||
remainingDeposit: '剩餘儲量',
|
||||
depletionTime: '預計耗盡',
|
||||
depositDepleted: '已耗盡',
|
||||
depositWarning: '警告:礦脈儲量即將耗盡(低於10%)!',
|
||||
depositDepletedMessage: '礦脈已耗盡,產量已停止。'
|
||||
},
|
||||
buildingDescriptions: {
|
||||
metalMine: '開採金屬資源',
|
||||
crystalMine: '開採晶體資源',
|
||||
deuteriumSynthesizer: '合成重氫資源',
|
||||
deuteriumSynthesizer: '合成重氫資源(溫度越低產量越高)',
|
||||
solarPlant: '提供能源',
|
||||
fusionReactor: '使用重氫產生大量能源',
|
||||
roboticsFactory: '加快建造速度',
|
||||
@@ -242,7 +261,7 @@ export default {
|
||||
colonyShip: '用於殖民新星球',
|
||||
recycler: '收集殘骸場資源',
|
||||
espionageProbe: '偵察敵方星球',
|
||||
solarSatellite: '提供額外能源,每個產生50點電力',
|
||||
solarSatellite: '提供額外能源,產能受星球溫度影響(溫度越高產能越高)',
|
||||
darkMatterHarvester: '專門用於採集暗物質的特殊飛船',
|
||||
deathstar: '終極武器,能夠摧毀整個行星'
|
||||
},
|
||||
@@ -369,8 +388,16 @@ export default {
|
||||
buildings: '建築',
|
||||
research: '研究',
|
||||
ships: '艦船',
|
||||
defense: '防禦'
|
||||
}
|
||||
defense: '防禦',
|
||||
waiting: '等待'
|
||||
},
|
||||
waitingEmpty: '目前沒有等待中的任務',
|
||||
addToWaiting: '加入等待佇列',
|
||||
remove: '移除',
|
||||
resourcesReady: '資源就緒',
|
||||
waitingResources: '等待資源',
|
||||
waitingQueueFull: '等待佇列已滿',
|
||||
movedToQueue: '任務已移至正式佇列'
|
||||
},
|
||||
overview: {
|
||||
title: '星球總覽',
|
||||
@@ -383,7 +410,10 @@ export default {
|
||||
consumptionSourcesDesc: '各建築的電力消耗詳情',
|
||||
totalProduction: '總產量',
|
||||
totalConsumption: '總消耗',
|
||||
noConsumption: '當前無電力消耗'
|
||||
noConsumption: '當前無電力消耗',
|
||||
tabOverview: '概覽',
|
||||
tabProduction: '產量詳情',
|
||||
tabConsumption: '消耗詳情'
|
||||
},
|
||||
buildingsView: {
|
||||
title: '建築',
|
||||
@@ -515,6 +545,29 @@ export default {
|
||||
spy: '偵察',
|
||||
deploy: '部署',
|
||||
expedition: '探險',
|
||||
expeditionZone: '探險區域',
|
||||
expeditionZoneDesc: '選擇探險目的地,不同區域有不同的風險和收益',
|
||||
requiresAstro: '需要天體物理學 {level} 級',
|
||||
reward: '收益',
|
||||
danger: '危險',
|
||||
zones: {
|
||||
nearSpace: {
|
||||
name: '近空區域',
|
||||
desc: '安全的近地空間,風險低但收益也較少'
|
||||
},
|
||||
deepSpace: {
|
||||
name: '深空區域',
|
||||
desc: '遠離恆星的深空,可能發現更多資源'
|
||||
},
|
||||
unchartedSpace: {
|
||||
name: '未知空間',
|
||||
desc: '未經探索的區域,高風險高回報'
|
||||
},
|
||||
dangerousNebula: {
|
||||
name: '危險星雲',
|
||||
desc: '充滿未知危險的星雲,但蘊含極其豐富的寶藏'
|
||||
}
|
||||
},
|
||||
recycle: '回收',
|
||||
transportResources: '運輸資源',
|
||||
totalCargoCapacity: '總載貨量',
|
||||
@@ -569,7 +622,24 @@ export default {
|
||||
presetName: '預設名稱',
|
||||
presetNamePlaceholder: '輸入預設名稱',
|
||||
deletePresetTitle: '刪除預設',
|
||||
deletePresetMessage: '確定要刪除預設「{name}」嗎?此操作不可撤銷。'
|
||||
deletePresetMessage: '確定要刪除預設「{name}」嗎?此操作不可撤銷。',
|
||||
// 跳躍門
|
||||
jumpGate: '跳躍門',
|
||||
jumpGateDescription: '使用跳躍門瞬間傳送艦隊到其他有跳躍門的月球',
|
||||
jumpGateNotAvailable: '跳躍門不可用',
|
||||
jumpGateRequiresMoon: '跳躍門只能在月球上使用',
|
||||
jumpGateNotBuilt: '當前月球沒有建造跳躍門',
|
||||
jumpGateCooldown: '跳躍門冷卻中',
|
||||
jumpGateCooldownRemaining: '剩餘冷卻時間',
|
||||
jumpGateReady: '跳躍門就緒',
|
||||
jumpGateSelectTarget: '選擇目標月球',
|
||||
jumpGateNoTargetMoons: '沒有可用的目標月球(需要有跳躍門且冷卻完成)',
|
||||
jumpGateSelectFleet: '選擇傳送艦隊',
|
||||
jumpGateTransfer: '傳送艦隊',
|
||||
jumpGateSuccess: '跳躍門傳送成功',
|
||||
jumpGateSuccessMessage: '艦隊已瞬間傳送到 {target}',
|
||||
jumpGateFailed: '跳躍門傳送失敗',
|
||||
jumpGateFailedMessage: '請檢查跳躍門狀態和艦隊配置'
|
||||
},
|
||||
officersView: {
|
||||
title: '軍官',
|
||||
@@ -628,6 +698,8 @@ export default {
|
||||
switch: '切換',
|
||||
recycle: '回收',
|
||||
debrisField: '殘骸場',
|
||||
oreDeposits: '礦脈儲量',
|
||||
deposits: '儲量',
|
||||
scoutPlanetTitle: '偵察星球',
|
||||
attackPlanetTitle: '攻擊星球',
|
||||
missileAttackTitle: '導彈攻擊',
|
||||
@@ -650,7 +722,9 @@ export default {
|
||||
debris: '殘骸',
|
||||
giftPlanetTitle: '贈送禮物',
|
||||
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。',
|
||||
npcPlanetName: '{name}的星球'
|
||||
npcPlanetName: '{name}的星球',
|
||||
intercepted: '被攔截',
|
||||
defenseLosses: '防禦損失'
|
||||
},
|
||||
messagesView: {
|
||||
title: '訊息中心',
|
||||
@@ -674,6 +748,8 @@ export default {
|
||||
attackerLosses: '攻擊方損失',
|
||||
defenderLosses: '防守方損失',
|
||||
noLosses: '無損失',
|
||||
losses: '損失統計',
|
||||
remainingUnits: '剩餘單位',
|
||||
plunder: '掠奪資源',
|
||||
debrisField: '殘骸場',
|
||||
resources: '資源',
|
||||
@@ -691,6 +767,18 @@ export default {
|
||||
round: '第{round}回合',
|
||||
attackerRemainingPower: '攻擊方剩餘火力',
|
||||
defenderRemainingPower: '防守方剩餘火力',
|
||||
// 戰鬥動畫相關
|
||||
playAnimation: '播放動畫',
|
||||
showDetails: '顯示詳情',
|
||||
speed: '速度',
|
||||
power: '戰鬥力',
|
||||
battleLogEmpty: '戰鬥日誌為空',
|
||||
roundStarted: '第{round}回合開始',
|
||||
shipDestroyed: '{count}艘{ship}被摧毀',
|
||||
defenseDestroyed: '{count}座{defense}被摧毀',
|
||||
attackerWins: '攻擊方獲勝',
|
||||
defenderWins: '防守方獲勝',
|
||||
roundsPlayed: '回合已播放',
|
||||
spied: '被偵查',
|
||||
spiedNotification: '被偵查通知',
|
||||
noSpiedNotifications: '暫無被偵查通知',
|
||||
@@ -899,6 +987,7 @@ export default {
|
||||
inAppNotifications: '頁面內通知',
|
||||
constructionComplete: '建築完成',
|
||||
researchComplete: '研究完成',
|
||||
unlockNotification: '解鎖通知',
|
||||
browserPermission: '啟用瀏覽器通知',
|
||||
permissionGranted: '已獲得權限',
|
||||
permissionDenied: '權限被拒絕/未獲得',
|
||||
@@ -906,11 +995,22 @@ export default {
|
||||
notificationsDisabled: '啟用上方任一開關以配置具體通知',
|
||||
suppressInFocus: '頁面聚焦時不發送瀏覽器通知',
|
||||
expandTypes: '展開詳細設定',
|
||||
collapseTypes: '收起詳細設定'
|
||||
collapseTypes: '收起詳細設定',
|
||||
// NPC名稱更新
|
||||
npcNameUpdate: 'NPC名稱更新',
|
||||
npcNameUpdateTitle: '檢測到舊版NPC名稱',
|
||||
npcNameUpdateMessage: '發現 {count} 個NPC使用舊版名稱格式。是否更新為新的本地化名稱?',
|
||||
npcNameUpdateConfirm: '更新名稱',
|
||||
npcNameUpdateCancel: '保持原樣',
|
||||
npcNameUpdateSuccess: '已成功更新 {count} 個NPC名稱',
|
||||
npcNameUpdateSkipped: '已跳過NPC名稱更新'
|
||||
},
|
||||
notifications: {
|
||||
constructionComplete: '建造完成',
|
||||
researchComplete: '研究完成'
|
||||
researchComplete: '研究完成',
|
||||
newUnlock: '新內容解鎖',
|
||||
building: '建築',
|
||||
technology: '科技'
|
||||
},
|
||||
gmView: {
|
||||
title: 'GM 管理面板',
|
||||
@@ -1146,6 +1246,21 @@ export default {
|
||||
attackCooldown: '攻擊冷卻中({min}分{sec}秒)',
|
||||
notSpiedYet: '尚未偵查過,需要先進行偵查'
|
||||
}
|
||||
},
|
||||
aiType: 'AI類型',
|
||||
aiTypes: {
|
||||
aggressive: '侵略型',
|
||||
defensive: '防守型',
|
||||
trader: '商人型',
|
||||
expansionist: '擴張型',
|
||||
balanced: '平衡型'
|
||||
},
|
||||
aiTypeDescriptions: {
|
||||
aggressive: '積極偵查和攻擊,反擊猛烈',
|
||||
defensive: '很少主動攻擊,被攻擊後強烈反擊',
|
||||
trader: '幾乎不攻擊,更願意交易和送禮',
|
||||
expansionist: '專注發展,較少攻擊',
|
||||
balanced: '根據情況動態調整策略'
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
@@ -1536,5 +1651,24 @@ export default {
|
||||
item4: '地質學家 - 增加資源產量'
|
||||
}
|
||||
}
|
||||
},
|
||||
ranking: {
|
||||
title: '排行榜',
|
||||
totalPlayers: '共 {count} 名玩家',
|
||||
yourRanking: '你的排名',
|
||||
categories: {
|
||||
total: '總積分',
|
||||
building: '建築',
|
||||
research: '研究',
|
||||
fleet: '艦隊',
|
||||
defense: '防禦'
|
||||
},
|
||||
points: '分',
|
||||
name: '名稱',
|
||||
planets: '星球',
|
||||
details: '詳情',
|
||||
you: '你',
|
||||
scoreBreakdown: '積分詳情',
|
||||
noData: '暫無排行數據'
|
||||
}
|
||||
}
|
||||
|
||||
603
src/logic/campaignLogic.ts
Normal file
603
src/logic/campaignLogic.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
/**
|
||||
* 战役系统逻辑
|
||||
* 处理任务进度、解锁、完成等核心逻辑
|
||||
*/
|
||||
|
||||
import {
|
||||
QuestStatus,
|
||||
ObjectiveType,
|
||||
type Player,
|
||||
type PlayerCampaignProgress,
|
||||
type QuestObjective,
|
||||
type QuestReward,
|
||||
type QuestNotification,
|
||||
type CampaignQuestConfig,
|
||||
type NPC,
|
||||
type Resources,
|
||||
RelationStatus,
|
||||
type BuildingType,
|
||||
type TechnologyType,
|
||||
type ShipType
|
||||
} from '@/types/game'
|
||||
import { MAIN_CAMPAIGN, getAllQuests, getQuestById, getQuestsByChapter } from '@/config/campaignConfig'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
|
||||
/**
|
||||
* 初始化玩家战役进度
|
||||
*/
|
||||
export const initializeCampaignProgress = (player: Player): PlayerCampaignProgress => {
|
||||
// 如果已有进度,返回现有进度
|
||||
if (player.campaignProgress) {
|
||||
return player.campaignProgress
|
||||
}
|
||||
|
||||
// 创建初始进度,第一个任务默认解锁
|
||||
const firstQuest = getQuestsByChapter(1)[0]
|
||||
const progress: PlayerCampaignProgress = {
|
||||
campaignId: MAIN_CAMPAIGN.id,
|
||||
currentChapter: 1,
|
||||
currentQuestId: firstQuest?.id,
|
||||
questProgress: {},
|
||||
completedQuests: [],
|
||||
unlockedQuests: firstQuest ? [firstQuest.id] : [],
|
||||
branchChoices: {}
|
||||
}
|
||||
|
||||
// 初始化第一个任务为可接取状态
|
||||
if (firstQuest) {
|
||||
progress.questProgress[firstQuest.id] = {
|
||||
questId: firstQuest.id,
|
||||
status: QuestStatus.Available,
|
||||
objectives: {}
|
||||
}
|
||||
// 初始化目标进度
|
||||
const questProgress = progress.questProgress[firstQuest.id]
|
||||
if (questProgress) {
|
||||
firstQuest.objectives.forEach(obj => {
|
||||
questProgress.objectives[obj.id] = {
|
||||
current: 0,
|
||||
completed: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*/
|
||||
export const getQuestStatus = (progress: PlayerCampaignProgress, questId: string): QuestStatus => {
|
||||
const questProgress = progress.questProgress[questId]
|
||||
if (questProgress) {
|
||||
return questProgress.status
|
||||
}
|
||||
|
||||
// 检查是否已解锁
|
||||
if (progress.unlockedQuests.includes(questId)) {
|
||||
return QuestStatus.Available
|
||||
}
|
||||
|
||||
return QuestStatus.Locked
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始任务
|
||||
*/
|
||||
export const startQuest = (player: Player, questId: string): { success: boolean; error?: string } => {
|
||||
const progress = player.campaignProgress
|
||||
if (!progress) {
|
||||
return { success: false, error: 'campaign.errors.notInitialized' }
|
||||
}
|
||||
|
||||
const quest = getQuestById(questId)
|
||||
if (!quest) {
|
||||
return { success: false, error: 'campaign.errors.questNotFound' }
|
||||
}
|
||||
|
||||
// 检查是否已解锁
|
||||
if (!progress.unlockedQuests.includes(questId)) {
|
||||
return { success: false, error: 'campaign.errors.questLocked' }
|
||||
}
|
||||
|
||||
// 检查是否已完成
|
||||
if (progress.completedQuests.includes(questId)) {
|
||||
return { success: false, error: 'campaign.errors.questAlreadyCompleted' }
|
||||
}
|
||||
|
||||
// 初始化或更新任务进度
|
||||
if (!progress.questProgress[questId]) {
|
||||
progress.questProgress[questId] = {
|
||||
questId,
|
||||
status: QuestStatus.Active,
|
||||
objectives: {},
|
||||
startedAt: Date.now()
|
||||
}
|
||||
}
|
||||
const questProgress = progress.questProgress[questId]
|
||||
if (questProgress) {
|
||||
questProgress.status = QuestStatus.Active
|
||||
questProgress.startedAt = Date.now()
|
||||
// 初始化目标进度
|
||||
quest.objectives.forEach(obj => {
|
||||
if (!questProgress.objectives[obj.id]) {
|
||||
questProgress.objectives[obj.id] = {
|
||||
current: 0,
|
||||
completed: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
progress.currentQuestId = questId
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个目标的进度
|
||||
*/
|
||||
export const checkObjectiveProgress = (player: Player, objective: QuestObjective, npcs: NPC[]): number => {
|
||||
switch (objective.type) {
|
||||
case ObjectiveType.BuildBuilding: {
|
||||
// 检查玩家所有星球上该建筑的最高等级
|
||||
const targetLevel = objective.targetSecondary as number
|
||||
const buildingType = objective.target as BuildingType
|
||||
const hasBuilding = player.planets.some(planet => {
|
||||
const level = planet.buildings[buildingType] || 0
|
||||
return level >= targetLevel
|
||||
})
|
||||
return hasBuilding ? 1 : 0
|
||||
}
|
||||
|
||||
case ObjectiveType.ResearchTech: {
|
||||
const targetLevel = objective.targetSecondary as number
|
||||
const techType = objective.target as TechnologyType
|
||||
const currentLevel = player.technologies[techType] || 0
|
||||
return currentLevel >= targetLevel ? 1 : 0
|
||||
}
|
||||
|
||||
case ObjectiveType.ProduceShips: {
|
||||
// 统计所有星球上该类型舰船的总数
|
||||
const shipType = objective.target as ShipType
|
||||
let totalShips = 0
|
||||
player.planets.forEach(planet => {
|
||||
totalShips += planet.fleet[shipType] || 0
|
||||
})
|
||||
return Math.min(totalShips, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.AccumulateResources: {
|
||||
// 统计所有星球的资源总量
|
||||
let totalResources = 0
|
||||
player.planets.forEach(planet => {
|
||||
totalResources += planet.resources.metal + planet.resources.crystal + planet.resources.deuterium
|
||||
})
|
||||
return Math.min(totalResources, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.Colonize: {
|
||||
// 统计殖民地数量(不包括母星)
|
||||
const colonyCount = player.planets.length - 1
|
||||
return Math.min(colonyCount, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.SpyTarget: {
|
||||
// 统计侦查报告数量
|
||||
const target = objective.target
|
||||
if (target === 'any') {
|
||||
return Math.min(player.spyReports.length, objective.required)
|
||||
} else if (target === 'hostile') {
|
||||
// 统计敌对NPC的侦查报告
|
||||
const hostileNpcIds = npcs
|
||||
.filter(npc => {
|
||||
const relation = npc.relations?.[player.id]
|
||||
return relation?.status === RelationStatus.Hostile
|
||||
})
|
||||
.map(npc => npc.id)
|
||||
const hostileReports = player.spyReports.filter(report => hostileNpcIds.includes(report.targetPlayerId || ''))
|
||||
return Math.min(hostileReports.length, objective.required)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
case ObjectiveType.SendGift: {
|
||||
// 从成就统计中获取送礼次数
|
||||
return Math.min(player.achievementStats?.giftsSent || 0, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.Expedition: {
|
||||
// 从成就统计中获取探险次数(使用 expeditionsTotal)
|
||||
return Math.min(player.achievementStats?.expeditionsTotal || 0, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.WinBattles: {
|
||||
const target = objective.target
|
||||
if (target === 'attack') {
|
||||
return Math.min(player.achievementStats?.attacksWon || 0, objective.required)
|
||||
} else if (target === 'defense') {
|
||||
// 使用 defensesSuccessful 代替 defensesWon
|
||||
return Math.min(player.achievementStats?.defensesSuccessful || 0, objective.required)
|
||||
}
|
||||
const totalWins = (player.achievementStats?.attacksWon || 0) + (player.achievementStats?.defensesSuccessful || 0)
|
||||
return Math.min(totalWins, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.RecycleDebris: {
|
||||
return Math.min(player.achievementStats?.recyclingMissions || 0, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.ReachRelation: {
|
||||
// 检查是否有任何NPC达到指定关系等级
|
||||
const targetStatus = objective.target as string
|
||||
const hasRelation = npcs.some(npc => {
|
||||
const relation = npc.relations?.[player.id]
|
||||
return relation?.status === targetStatus
|
||||
})
|
||||
return hasRelation ? 1 : 0
|
||||
}
|
||||
|
||||
case ObjectiveType.FormAlliance: {
|
||||
// 检查是否有盟友(使用 Friendly 作为最高关系等级)
|
||||
const hasAlliance = npcs.some(npc => {
|
||||
const relation = npc.relations?.[player.id]
|
||||
return relation?.status === RelationStatus.Friendly && (relation.reputation || 0) >= 80
|
||||
})
|
||||
return hasAlliance ? 1 : 0
|
||||
}
|
||||
|
||||
case ObjectiveType.DefeatNPC: {
|
||||
// 检查攻击NPC获胜次数
|
||||
return Math.min(player.achievementStats?.attacksWon || 0, objective.required)
|
||||
}
|
||||
|
||||
case ObjectiveType.DestroyPlanet: {
|
||||
return Math.min(player.achievementStats?.planetDestructions || 0, objective.required)
|
||||
}
|
||||
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*/
|
||||
export const updateQuestProgress = (
|
||||
player: Player,
|
||||
questId: string,
|
||||
npcs: NPC[]
|
||||
): {
|
||||
updated: boolean
|
||||
completedObjectives: string[]
|
||||
questCompleted: boolean
|
||||
} => {
|
||||
const result = {
|
||||
updated: false,
|
||||
completedObjectives: [] as string[],
|
||||
questCompleted: false
|
||||
}
|
||||
|
||||
const progress = player.campaignProgress
|
||||
if (!progress) return result
|
||||
|
||||
const questProgress = progress.questProgress[questId]
|
||||
if (!questProgress || questProgress.status !== QuestStatus.Active) return result
|
||||
|
||||
const quest = getQuestById(questId)
|
||||
if (!quest) return result
|
||||
|
||||
// 检查每个目标的进度
|
||||
quest.objectives.forEach(objective => {
|
||||
const objProgress = questProgress.objectives[objective.id]
|
||||
if (!objProgress || objProgress.completed) return
|
||||
|
||||
const newProgress = checkObjectiveProgress(player, objective, npcs)
|
||||
if (newProgress !== objProgress.current) {
|
||||
objProgress.current = newProgress
|
||||
result.updated = true
|
||||
|
||||
// 检查目标是否完成
|
||||
if (newProgress >= objective.required && !objProgress.completed) {
|
||||
objProgress.completed = true
|
||||
result.completedObjectives.push(objective.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 检查任务是否全部完成
|
||||
const allCompleted = quest.objectives.every(obj => questProgress.objectives[obj.id]?.completed)
|
||||
|
||||
if (allCompleted && questProgress.status === QuestStatus.Active) {
|
||||
questProgress.status = QuestStatus.Completed
|
||||
questProgress.completedAt = Date.now()
|
||||
result.questCompleted = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取任务奖励
|
||||
*/
|
||||
export const claimQuestRewards = (
|
||||
player: Player,
|
||||
questId: string
|
||||
): {
|
||||
success: boolean
|
||||
rewards?: QuestReward
|
||||
error?: string
|
||||
} => {
|
||||
const progress = player.campaignProgress
|
||||
if (!progress) {
|
||||
return { success: false, error: 'campaign.errors.notInitialized' }
|
||||
}
|
||||
|
||||
const questProgress = progress.questProgress[questId]
|
||||
if (!questProgress) {
|
||||
return { success: false, error: 'campaign.errors.questNotFound' }
|
||||
}
|
||||
|
||||
if (questProgress.status !== QuestStatus.Completed) {
|
||||
return { success: false, error: 'campaign.errors.questNotCompleted' }
|
||||
}
|
||||
|
||||
if (questProgress.rewardsClaimed) {
|
||||
return { success: false, error: 'campaign.errors.rewardsAlreadyClaimed' }
|
||||
}
|
||||
|
||||
const quest = getQuestById(questId)
|
||||
if (!quest) {
|
||||
return { success: false, error: 'campaign.errors.questNotFound' }
|
||||
}
|
||||
|
||||
// 发放奖励
|
||||
const rewards = quest.rewards
|
||||
const currentPlanet = player.planets[0] // 默认发放到第一个星球
|
||||
|
||||
if (rewards.resources && currentPlanet) {
|
||||
resourceLogic.addResources(currentPlanet.resources, rewards.resources as Resources)
|
||||
}
|
||||
|
||||
if (rewards.darkMatter && currentPlanet) {
|
||||
currentPlanet.resources.darkMatter += rewards.darkMatter
|
||||
}
|
||||
|
||||
if (rewards.points) {
|
||||
player.points += rewards.points
|
||||
}
|
||||
|
||||
if (rewards.ships && currentPlanet) {
|
||||
Object.entries(rewards.ships).forEach(([shipType, count]) => {
|
||||
const ship = shipType as ShipType
|
||||
currentPlanet.fleet[ship] = (currentPlanet.fleet[ship] || 0) + count
|
||||
})
|
||||
}
|
||||
|
||||
// 标记奖励已领取
|
||||
questProgress.rewardsClaimed = true
|
||||
|
||||
// 将任务添加到已完成列表
|
||||
if (!progress.completedQuests.includes(questId)) {
|
||||
progress.completedQuests.push(questId)
|
||||
}
|
||||
|
||||
// 解锁后续任务
|
||||
unlockNextQuests(player, questId)
|
||||
|
||||
return { success: true, rewards }
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁后续任务
|
||||
*/
|
||||
export const unlockNextQuests = (player: Player, _completedQuestId: string): string[] => {
|
||||
const progress = player.campaignProgress
|
||||
if (!progress) return []
|
||||
|
||||
const unlockedQuests: string[] = []
|
||||
const allQuests = getAllQuests()
|
||||
|
||||
allQuests.forEach(quest => {
|
||||
// 跳过已解锁的任务
|
||||
if (progress.unlockedQuests.includes(quest.id)) return
|
||||
|
||||
// 检查前置任务是否完成
|
||||
if (quest.requiredQuestIds && quest.requiredQuestIds.length > 0) {
|
||||
const allRequiredCompleted = quest.requiredQuestIds.every(reqId => progress.completedQuests.includes(reqId))
|
||||
|
||||
if (allRequiredCompleted) {
|
||||
progress.unlockedQuests.push(quest.id)
|
||||
unlockedQuests.push(quest.id)
|
||||
|
||||
// 初始化任务进度
|
||||
progress.questProgress[quest.id] = {
|
||||
questId: quest.id,
|
||||
status: QuestStatus.Available,
|
||||
objectives: {}
|
||||
}
|
||||
const questProgress = progress.questProgress[quest.id]
|
||||
if (questProgress) {
|
||||
quest.objectives.forEach(obj => {
|
||||
questProgress.objectives[obj.id] = {
|
||||
current: 0,
|
||||
completed: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新当前章节
|
||||
if (quest.chapter > progress.currentChapter) {
|
||||
progress.currentChapter = quest.chapter
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return unlockedQuests
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算战役总进度百分比
|
||||
*/
|
||||
export const calculateCampaignProgress = (progress: PlayerCampaignProgress): number => {
|
||||
const totalQuests = getAllQuests().length
|
||||
const completedCount = progress.completedQuests.length
|
||||
return Math.round((completedCount / totalQuests) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算章节进度百分比
|
||||
*/
|
||||
export const calculateChapterProgress = (progress: PlayerCampaignProgress, chapterNumber: number): number => {
|
||||
const chapterQuests = getQuestsByChapter(chapterNumber)
|
||||
const completedCount = chapterQuests.filter(quest => progress.completedQuests.includes(quest.id)).length
|
||||
return Math.round((completedCount / chapterQuests.length) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个任务的进度百分比
|
||||
*/
|
||||
export const calculateQuestProgress = (progress: PlayerCampaignProgress, questId: string): number => {
|
||||
const questProgress = progress.questProgress[questId]
|
||||
if (!questProgress) return 0
|
||||
|
||||
const quest = getQuestById(questId)
|
||||
if (!quest) return 0
|
||||
|
||||
// 计算所有目标的平均进度
|
||||
const objectives = quest.objectives
|
||||
if (objectives.length === 0) return 0
|
||||
|
||||
let totalProgress = 0
|
||||
objectives.forEach(obj => {
|
||||
const objProgress = questProgress.objectives[obj.id]
|
||||
if (objProgress) {
|
||||
if (objProgress.completed) {
|
||||
totalProgress += 100
|
||||
} else {
|
||||
totalProgress += Math.min((objProgress.current / obj.required) * 100, 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Math.round(totalProgress / objectives.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前可进行的任务列表
|
||||
*/
|
||||
export const getAvailableQuests = (progress: PlayerCampaignProgress): CampaignQuestConfig[] => {
|
||||
return getAllQuests().filter(quest => {
|
||||
const status = getQuestStatus(progress, quest.id)
|
||||
return status === QuestStatus.Available || status === QuestStatus.Active
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活的任务
|
||||
*/
|
||||
export const getActiveQuest = (progress: PlayerCampaignProgress): CampaignQuestConfig | undefined => {
|
||||
if (!progress.currentQuestId) return undefined
|
||||
return getQuestById(progress.currentQuestId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务通知
|
||||
*/
|
||||
export const createQuestNotification = (
|
||||
questId: string,
|
||||
eventType: QuestNotification['eventType'],
|
||||
messageKey: string,
|
||||
messageParams?: Record<string, string | number>,
|
||||
rewards?: QuestReward
|
||||
): QuestNotification => {
|
||||
const quest = getQuestById(questId)
|
||||
return {
|
||||
id: `quest_notification_${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
questId,
|
||||
questTitleKey: quest?.titleKey || '',
|
||||
eventType,
|
||||
messageKey,
|
||||
messageParams,
|
||||
rewards,
|
||||
read: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务通知到玩家
|
||||
*/
|
||||
export const addQuestNotification = (player: Player, notification: QuestNotification): void => {
|
||||
if (!player.questNotifications) {
|
||||
player.questNotifications = []
|
||||
}
|
||||
player.questNotifications.unshift(notification)
|
||||
|
||||
// 限制通知数量
|
||||
if (player.questNotifications.length > 50) {
|
||||
player.questNotifications = player.questNotifications.slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记通知为已读
|
||||
*/
|
||||
export const markQuestNotificationRead = (player: Player, notificationId: string): void => {
|
||||
const notification = player.questNotifications?.find(n => n.id === notificationId)
|
||||
if (notification) {
|
||||
notification.read = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读通知数量
|
||||
*/
|
||||
export const getUnreadQuestNotificationCount = (player: Player): number => {
|
||||
return player.questNotifications?.filter(n => !n.read).length || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并更新所有激活任务的进度
|
||||
*/
|
||||
export const checkAllActiveQuestsProgress = (player: Player, npcs: NPC[]): QuestNotification[] => {
|
||||
const notifications: QuestNotification[] = []
|
||||
const progress = player.campaignProgress
|
||||
|
||||
if (!progress) return notifications
|
||||
|
||||
// 遍历所有激活的任务
|
||||
Object.entries(progress.questProgress).forEach(([questId, questProgress]) => {
|
||||
if (questProgress.status !== QuestStatus.Active) return
|
||||
|
||||
const result = updateQuestProgress(player, questId, npcs)
|
||||
|
||||
// 为完成的目标创建通知
|
||||
result.completedObjectives.forEach(objId => {
|
||||
const quest = getQuestById(questId)
|
||||
const objective = quest?.objectives.find(o => o.id === objId)
|
||||
if (objective) {
|
||||
notifications.push(
|
||||
createQuestNotification(questId, 'objective_completed', 'campaign.notifications.objectiveCompleted', {
|
||||
objective: objective.descriptionKey
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// 任务完成通知
|
||||
if (result.questCompleted) {
|
||||
const quest = getQuestById(questId)
|
||||
notifications.push(createQuestNotification(questId, 'quest_completed', 'campaign.notifications.questCompleted', {}, quest?.rewards))
|
||||
}
|
||||
})
|
||||
|
||||
// 添加通知到玩家
|
||||
notifications.forEach(notification => {
|
||||
addQuestNotification(player, notification)
|
||||
})
|
||||
|
||||
return notifications
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FleetMission, Planet, Resources, Fleet, BattleResult, SpyReport, Player, Officer, DebrisField, NPC } from '@/types/game'
|
||||
import type { Locale } from '@/locales'
|
||||
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType } from '@/types/game'
|
||||
import { FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
|
||||
import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType, ExpeditionZone } from '@/types/game'
|
||||
import { FLEET_STORAGE_CONFIG, EXPEDITION_ZONES } from '@/config/gameConfig'
|
||||
import * as battleLogic from './battleLogic'
|
||||
import * as moonLogic from './moonLogic'
|
||||
import * as moonValidation from './moonValidation'
|
||||
@@ -9,6 +9,7 @@ import * as diplomaticLogic from './diplomaticLogic'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
import * as fleetStorageLogic from './fleetStorageLogic'
|
||||
import * as officerLogic from './officerLogic'
|
||||
import * as planetLogic from './planetLogic'
|
||||
|
||||
/**
|
||||
* 计算两个星球之间的距离
|
||||
@@ -444,6 +445,7 @@ export const processColonizeArrival = (
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
waitingBuildQueue: [], // 等待队列
|
||||
lastUpdate: Date.now(),
|
||||
maxSpace: 200,
|
||||
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
|
||||
@@ -454,6 +456,9 @@ export const processColonizeArrival = (
|
||||
newPlanet.buildings[building] = 0
|
||||
})
|
||||
|
||||
// 初始化温度
|
||||
newPlanet.temperature = planetLogic.generatePlanetTemperature(mission.targetPosition.position)
|
||||
|
||||
// 殖民船被消耗
|
||||
mission.fleet[ShipType.ColonyShip] = (mission.fleet[ShipType.ColonyShip] || 1) - 1
|
||||
mission.status = 'returning'
|
||||
@@ -732,9 +737,13 @@ export interface ExpeditionResult {
|
||||
|
||||
/**
|
||||
* 处理远征任务到达
|
||||
* 远征任务会随机触发各种事件
|
||||
* 远征任务会随机触发各种事件,基于探险区域配置
|
||||
*/
|
||||
export const processExpeditionArrival = (mission: FleetMission): ExpeditionResult => {
|
||||
// 获取探险区域配置,默认为近空区域
|
||||
const zone = mission.expeditionZone || ExpeditionZone.NearSpace
|
||||
const zoneConfig = EXPEDITION_ZONES[zone]
|
||||
|
||||
// 计算舰队总货舱容量
|
||||
let totalCargoCapacity = 0
|
||||
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||
@@ -744,13 +753,22 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
}
|
||||
}
|
||||
|
||||
// 随机事件概率
|
||||
// 根据区域配置的概率计算随机事件
|
||||
const random = Math.random() * 100
|
||||
const probs = zoneConfig.probabilities
|
||||
let result: ExpeditionResult
|
||||
|
||||
if (random < 30) {
|
||||
// 30% 概率发现资源
|
||||
const resourceMultiplier = 0.1 + Math.random() * 0.3 // 10%-40% 的货舱容量
|
||||
// 累积概率阈值
|
||||
const resourceThreshold = probs.resources
|
||||
const darkMatterThreshold = resourceThreshold + probs.darkMatter
|
||||
const fleetThreshold = darkMatterThreshold + probs.fleet
|
||||
const piratesThreshold = fleetThreshold + probs.pirates
|
||||
const aliensThreshold = piratesThreshold + probs.aliens
|
||||
|
||||
if (random < resourceThreshold) {
|
||||
// 发现资源
|
||||
const baseMultiplier = 0.1 + Math.random() * 0.3 // 10%-40% 的货舱容量
|
||||
const resourceMultiplier = baseMultiplier * zoneConfig.resourceMultiplier
|
||||
const resourceAmount = Math.floor(totalCargoCapacity * resourceMultiplier)
|
||||
const metalAmount = Math.floor(resourceAmount * 0.5)
|
||||
const crystalAmount = Math.floor(resourceAmount * 0.35)
|
||||
@@ -765,9 +783,10 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
resources: { metal: metalAmount, crystal: crystalAmount, deuterium: deuteriumAmount, darkMatter: 0, energy: 0 },
|
||||
message: 'expedition.foundResources'
|
||||
}
|
||||
} else if (random < 40) {
|
||||
// 10% 概率发现暗物质
|
||||
const darkMatterAmount = Math.floor(50 + Math.random() * 150) // 50-200 暗物质
|
||||
} else if (random < darkMatterThreshold) {
|
||||
// 发现暗物质
|
||||
const baseDarkMatter = 50 + Math.random() * 150 // 50-200 暗物质
|
||||
const darkMatterAmount = Math.floor(baseDarkMatter * zoneConfig.darkMatterMultiplier)
|
||||
mission.cargo.darkMatter += darkMatterAmount
|
||||
|
||||
result = {
|
||||
@@ -775,13 +794,21 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
resources: { metal: 0, crystal: 0, deuterium: 0, darkMatter: darkMatterAmount, energy: 0 },
|
||||
message: 'expedition.foundDarkMatter'
|
||||
}
|
||||
} else if (random < 55) {
|
||||
// 15% 概率发现废弃舰船
|
||||
} else if (random < fleetThreshold) {
|
||||
// 发现废弃舰船
|
||||
const foundFleet: Partial<Fleet> = {}
|
||||
const possibleShips: ShipType[] = [ShipType.LightFighter, ShipType.HeavyFighter, ShipType.SmallCargo, ShipType.LargeCargo]
|
||||
// 高级区域可以发现更多种类的舰船
|
||||
const possibleShips: ShipType[] =
|
||||
zone === ExpeditionZone.DangerousNebula
|
||||
? [ShipType.LightFighter, ShipType.HeavyFighter, ShipType.SmallCargo, ShipType.LargeCargo, ShipType.Cruiser, ShipType.Battleship]
|
||||
: zone === ExpeditionZone.UnchartedSpace
|
||||
? [ShipType.LightFighter, ShipType.HeavyFighter, ShipType.SmallCargo, ShipType.LargeCargo, ShipType.Cruiser]
|
||||
: [ShipType.LightFighter, ShipType.HeavyFighter, ShipType.SmallCargo, ShipType.LargeCargo]
|
||||
|
||||
const shipTypeIndex = Math.floor(Math.random() * possibleShips.length)
|
||||
const shipType = possibleShips[shipTypeIndex] ?? ShipType.LightFighter
|
||||
const count = Math.floor(1 + Math.random() * 5) // 1-5 艘
|
||||
const baseCount = 1 + Math.random() * 5 // 1-5 艘
|
||||
const count = Math.floor(baseCount * zoneConfig.fleetFindMultiplier)
|
||||
foundFleet[shipType] = count
|
||||
|
||||
// 将发现的舰船添加到任务舰队中
|
||||
@@ -792,15 +819,16 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
fleet: foundFleet,
|
||||
message: 'expedition.foundFleet'
|
||||
}
|
||||
} else if (random < 70) {
|
||||
// 15% 概率遭遇海盗(损失部分舰队)
|
||||
} else if (random < piratesThreshold) {
|
||||
// 遭遇海盗(损失部分舰队)
|
||||
const fleetLost: Partial<Fleet> = {}
|
||||
let hasLoss = false
|
||||
const lossChance = Math.min(0.3 * zoneConfig.dangerMultiplier, 0.9) // 危险区域损失概率更高,上限90%
|
||||
|
||||
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||
if (count > 0 && Math.random() < 0.3) {
|
||||
// 30% 概率损失该类型舰船
|
||||
const lossCount = Math.max(1, Math.floor(count * 0.1)) // 损失10%,最少1艘
|
||||
if (count > 0 && Math.random() < lossChance) {
|
||||
const baseLossRate = Math.min(0.1 * zoneConfig.dangerMultiplier, 0.5) // 危险区域损失比例更高,上限50%
|
||||
const lossCount = Math.max(1, Math.floor(count * baseLossRate))
|
||||
const actualLoss = Math.min(lossCount, count)
|
||||
fleetLost[shipType as ShipType] = actualLoss
|
||||
mission.fleet[shipType as ShipType] = count - actualLoss
|
||||
@@ -813,15 +841,16 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
fleetLost: hasLoss ? fleetLost : undefined,
|
||||
message: hasLoss ? 'expedition.piratesAttack' : 'expedition.piratesEscaped'
|
||||
}
|
||||
} else if (random < 80) {
|
||||
// 10% 概率遭遇外星人(损失更多舰队)
|
||||
} else if (random < aliensThreshold) {
|
||||
// 遭遇外星人(损失更多舰队)
|
||||
const fleetLost: Partial<Fleet> = {}
|
||||
let hasLoss = false
|
||||
const lossChance = Math.min(0.5 * zoneConfig.dangerMultiplier, 0.95) // 危险区域损失概率更高,上限95%
|
||||
|
||||
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||
if (count > 0 && Math.random() < 0.5) {
|
||||
// 50% 概率损失该类型舰船
|
||||
const lossCount = Math.max(1, Math.floor(count * 0.2)) // 损失20%,最少1艘
|
||||
if (count > 0 && Math.random() < lossChance) {
|
||||
const baseLossRate = Math.min(0.2 * zoneConfig.dangerMultiplier, 0.6) // 危险区域损失比例更高,上限60%
|
||||
const lossCount = Math.max(1, Math.floor(count * baseLossRate))
|
||||
const actualLoss = Math.min(lossCount, count)
|
||||
fleetLost[shipType as ShipType] = actualLoss
|
||||
mission.fleet[shipType as ShipType] = count - actualLoss
|
||||
@@ -835,7 +864,7 @@ export const processExpeditionArrival = (mission: FleetMission): ExpeditionResul
|
||||
message: hasLoss ? 'expedition.aliensAttack' : 'expedition.aliensEscaped'
|
||||
}
|
||||
} else {
|
||||
// 20% 概率什么都没发现
|
||||
// 什么都没发现
|
||||
result = {
|
||||
eventType: 'nothing',
|
||||
message: 'expedition.nothing'
|
||||
@@ -940,11 +969,7 @@ export interface DestroyResult {
|
||||
failReason?: DestroyFailReason // 失败原因
|
||||
}
|
||||
|
||||
export const processDestroyArrival = (
|
||||
mission: FleetMission,
|
||||
targetPlanet: Planet | undefined,
|
||||
attacker: Player
|
||||
): DestroyResult => {
|
||||
export const processDestroyArrival = (mission: FleetMission, targetPlanet: Planet | undefined, attacker: Player): DestroyResult => {
|
||||
if (!targetPlanet) {
|
||||
mission.status = 'returning'
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,9 @@ import * as pointsLogic from './pointsLogic'
|
||||
import * as planetLogic from './planetLogic'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
import * as achievementLogic from './achievementLogic'
|
||||
import * as unlockLogic from './unlockLogic'
|
||||
import * as waitingQueueLogic from './waitingQueueLogic'
|
||||
import type { UnlockedItem } from './unlockLogic'
|
||||
|
||||
/**
|
||||
* 初始化玩家数据
|
||||
@@ -19,6 +22,7 @@ export const initializePlayer = (playerId: string, playerName: string = 'Command
|
||||
technologies: {} as Record<TechnologyType, number>,
|
||||
officers: {} as Record<OfficerType, Officer>,
|
||||
researchQueue: [],
|
||||
waitingResearchQueue: [], // 研究等待队列
|
||||
fleetMissions: [],
|
||||
missileAttacks: [],
|
||||
battleReports: [],
|
||||
@@ -102,7 +106,8 @@ export const processGameUpdate = (
|
||||
player: Player,
|
||||
now: number,
|
||||
gameSpeed: number = 1,
|
||||
onNotification?: (type: string, itemType: string, level?: number) => void
|
||||
onNotification?: (type: string, itemType: string, level?: number) => void,
|
||||
onUnlock?: (unlockedItems: UnlockedItem[]) => void
|
||||
): {
|
||||
updatedResearchQueue: BuildQueueItem[]
|
||||
} => {
|
||||
@@ -122,6 +127,9 @@ export const processGameUpdate = (
|
||||
pointsLogic.addPoints(player, points)
|
||||
}
|
||||
|
||||
// 保存完成前的状态(用于解锁检查)
|
||||
const previousTechnologies = { ...player.technologies }
|
||||
|
||||
// 通知回调 + 成就统计更新
|
||||
const onCompleted = (type: string, itemType: string, level?: number, quantity?: number) => {
|
||||
if (onNotification) {
|
||||
@@ -166,11 +174,23 @@ export const processGameUpdate = (
|
||||
}
|
||||
})
|
||||
|
||||
// 收集所有新解锁的内容
|
||||
const allUnlockedItems: UnlockedItem[] = []
|
||||
|
||||
// 更新所有星球其他状态
|
||||
player.planets.forEach(planet => {
|
||||
// 保存完成前的建筑状态
|
||||
const previousBuildings = { ...planet.buildings }
|
||||
|
||||
// 检查建造队列
|
||||
buildingLogic.completeBuildQueue(planet, now, onPointsEarned, onCompleted)
|
||||
|
||||
// 检查新解锁(只在主星球上检查,避免重复通知)
|
||||
if (!planet.isMoon && onUnlock) {
|
||||
const unlockedItems = unlockLogic.checkAllNewlyUnlocked(planet, player.technologies, previousBuildings, previousTechnologies)
|
||||
allUnlockedItems.push(...unlockedItems)
|
||||
}
|
||||
|
||||
// 更新星球最大空间
|
||||
if (planet.isMoon) {
|
||||
planet.maxSpace = planetLogic.calculateMoonMaxSpace(planet)
|
||||
@@ -189,6 +209,41 @@ export const processGameUpdate = (
|
||||
onCompleted
|
||||
)
|
||||
|
||||
// 处理等待队列自动执行
|
||||
const waitingResult = waitingQueueLogic.processAllWaitingQueues(player, now)
|
||||
// 如果有等待队列项被执行,可以在这里添加通知(可选)
|
||||
if (waitingResult.executed.length > 0 && onNotification) {
|
||||
waitingResult.executed.forEach(item => {
|
||||
// 通知等待队列项已移至正式队列
|
||||
onNotification('waitingQueueMoved', item.itemType as string, item.targetLevel || item.quantity)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果科技完成,再次检查解锁(使用第一个非月球星球)
|
||||
if (onUnlock && player.technologies !== previousTechnologies) {
|
||||
const mainPlanet = player.planets.find(p => !p.isMoon)
|
||||
if (mainPlanet) {
|
||||
// 注意:这里使用完成后的建筑状态,因为我们只关心科技完成带来的解锁
|
||||
const techUnlockedItems = unlockLogic.checkAllNewlyUnlocked(
|
||||
mainPlanet,
|
||||
player.technologies,
|
||||
mainPlanet.buildings,
|
||||
previousTechnologies
|
||||
)
|
||||
// 去重(避免与建筑完成时的解锁重复)
|
||||
techUnlockedItems.forEach(item => {
|
||||
if (!allUnlockedItems.some(existing => existing.type === item.type && existing.id === item.id)) {
|
||||
allUnlockedItems.push(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 触发解锁通知
|
||||
if (onUnlock && allUnlockedItems.length > 0) {
|
||||
onUnlock(allUnlockedItems)
|
||||
}
|
||||
|
||||
return {
|
||||
updatedResearchQueue
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export const tryGenerateMoon = (
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
waitingBuildQueue: [], // 等待队列
|
||||
lastUpdate: Date.now(),
|
||||
maxSpace: MOON_CONFIG.baseFields, // OGame规则:月球初始只有1格空间
|
||||
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
|
||||
|
||||
@@ -1,9 +1,116 @@
|
||||
import type { NPC, Planet, Player, FleetMission, SpyReport, SpiedNotification, IncomingFleetAlert, Fleet, DebrisField } from '@/types/game'
|
||||
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
||||
import { MissionType, ShipType, TechnologyType, RelationStatus, NPCAIType } from '@/types/game'
|
||||
import * as fleetLogic from './fleetLogic'
|
||||
import * as diplomaticLogic from './diplomaticLogic'
|
||||
import { DIPLOMATIC_CONFIG, SHIPS } from '@/config/gameConfig'
|
||||
|
||||
/**
|
||||
* AI 类型行为修改器
|
||||
* 根据 NPC 的 AI 类型调整其行为参数
|
||||
*/
|
||||
export interface AIBehaviorModifier {
|
||||
spyFrequencyMultiplier: number // 侦查频率倍率(越高侦查越频繁)
|
||||
attackFrequencyMultiplier: number // 攻击频率倍率
|
||||
attackProbabilityMultiplier: number // 攻击概率倍率
|
||||
fleetSizeMultiplier: number // 出击舰队比例倍率
|
||||
giftProbabilityMultiplier: number // 赠送礼物概率倍率
|
||||
revengeMultiplier: number // 反击倾向倍率
|
||||
defenseFocus: boolean // 是否优先发展防御
|
||||
willAttackWhenHostile: boolean // 敌对时是否会主动攻击
|
||||
willSpyWhenHostile: boolean // 敌对时是否会侦查
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 类型的行为修改器
|
||||
*/
|
||||
export const getAIBehaviorModifier = (aiType?: NPCAIType): AIBehaviorModifier => {
|
||||
switch (aiType) {
|
||||
case NPCAIType.Aggressive:
|
||||
// 侵略型:高频侦查攻击,大舰队出击,不送礼,强烈反击
|
||||
return {
|
||||
spyFrequencyMultiplier: 1.5,
|
||||
attackFrequencyMultiplier: 1.5,
|
||||
attackProbabilityMultiplier: 1.3,
|
||||
fleetSizeMultiplier: 1.2,
|
||||
giftProbabilityMultiplier: 0,
|
||||
revengeMultiplier: 2.0,
|
||||
defenseFocus: false,
|
||||
willAttackWhenHostile: true,
|
||||
willSpyWhenHostile: true
|
||||
}
|
||||
|
||||
case NPCAIType.Defensive:
|
||||
// 防守型:低频侦查,几乎不主动攻击,但被攻击后强烈反击
|
||||
return {
|
||||
spyFrequencyMultiplier: 0.3,
|
||||
attackFrequencyMultiplier: 0.1,
|
||||
attackProbabilityMultiplier: 0.1,
|
||||
fleetSizeMultiplier: 0.5,
|
||||
giftProbabilityMultiplier: 0.5,
|
||||
revengeMultiplier: 3.0, // 反击很强烈
|
||||
defenseFocus: true,
|
||||
willAttackWhenHostile: false, // 即使敌对也不主动攻击
|
||||
willSpyWhenHostile: true // 但会侦查监视
|
||||
}
|
||||
|
||||
case NPCAIType.Trader:
|
||||
// 商人型:几乎不攻击,高概率送礼,被攻击后轻微反击
|
||||
return {
|
||||
spyFrequencyMultiplier: 0.2,
|
||||
attackFrequencyMultiplier: 0.05,
|
||||
attackProbabilityMultiplier: 0.05,
|
||||
fleetSizeMultiplier: 0.3,
|
||||
giftProbabilityMultiplier: 3.0, // 高概率送礼
|
||||
revengeMultiplier: 0.5, // 反击意愿低
|
||||
defenseFocus: false,
|
||||
willAttackWhenHostile: false,
|
||||
willSpyWhenHostile: false
|
||||
}
|
||||
|
||||
case NPCAIType.Expansionist:
|
||||
// 扩张型:中等侦查频率,较少攻击,专注发展
|
||||
return {
|
||||
spyFrequencyMultiplier: 0.7,
|
||||
attackFrequencyMultiplier: 0.4,
|
||||
attackProbabilityMultiplier: 0.5,
|
||||
fleetSizeMultiplier: 0.6,
|
||||
giftProbabilityMultiplier: 1.0,
|
||||
revengeMultiplier: 1.0,
|
||||
defenseFocus: false,
|
||||
willAttackWhenHostile: true,
|
||||
willSpyWhenHostile: true
|
||||
}
|
||||
|
||||
case NPCAIType.Balanced:
|
||||
default:
|
||||
// 平衡型(默认):标准行为
|
||||
return {
|
||||
spyFrequencyMultiplier: 1.0,
|
||||
attackFrequencyMultiplier: 1.0,
|
||||
attackProbabilityMultiplier: 1.0,
|
||||
fleetSizeMultiplier: 1.0,
|
||||
giftProbabilityMultiplier: 1.0,
|
||||
revengeMultiplier: 1.0,
|
||||
defenseFocus: false,
|
||||
willAttackWhenHostile: true,
|
||||
willSpyWhenHostile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 AI 类型调整动态行为配置
|
||||
*/
|
||||
export const applyAIModifierToConfig = (config: DynamicBehaviorConfig, modifier: AIBehaviorModifier): DynamicBehaviorConfig => {
|
||||
return {
|
||||
...config,
|
||||
spyInterval: Math.floor(config.spyInterval / modifier.spyFrequencyMultiplier),
|
||||
attackInterval: Math.floor(config.attackInterval / modifier.attackFrequencyMultiplier),
|
||||
attackProbability: Math.min(1.0, config.attackProbability * modifier.attackProbabilityMultiplier),
|
||||
attackFleetSizeRatio: Math.min(1.0, config.attackFleetSizeRatio * modifier.fleetSizeMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC行为决策系统
|
||||
*
|
||||
@@ -100,6 +207,9 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取 AI 行为修改器
|
||||
const aiModifier = getAIBehaviorModifier(npc.aiType)
|
||||
|
||||
// 检查外交关系 - 统一使用 npc.relations
|
||||
const relation = npc.relations?.[player.id]
|
||||
|
||||
@@ -118,9 +228,15 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有敌对NPC才会到达这里,检查冷却时间
|
||||
// 根据 AI 类型判断是否会侦查
|
||||
if (!aiModifier.willSpyWhenHostile) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有敌对NPC才会到达这里,检查冷却时间(根据 AI 类型调整)
|
||||
const adjustedConfig = applyAIModifierToConfig(config, aiModifier)
|
||||
const lastSpyTime = npc.lastSpyTime || 0
|
||||
if (currentTime - lastSpyTime < config.spyInterval * 1000) {
|
||||
if (currentTime - lastSpyTime < adjustedConfig.spyInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -138,6 +254,9 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取 AI 行为修改器
|
||||
const aiModifier = getAIBehaviorModifier(npc.aiType)
|
||||
|
||||
// 检查外交关系 - 统一使用 npc.relations
|
||||
const relation = npc.relations?.[player.id]
|
||||
|
||||
@@ -156,9 +275,15 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查攻击冷却
|
||||
// 根据 AI 类型判断是否会主动攻击
|
||||
if (!aiModifier.willAttackWhenHostile) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查攻击冷却(根据 AI 类型调整)
|
||||
const adjustedConfig = applyAIModifierToConfig(config, aiModifier)
|
||||
const lastAttackTime = npc.lastAttackTime || 0
|
||||
if (currentTime - lastAttackTime < config.attackInterval * 1000) {
|
||||
if (currentTime - lastAttackTime < adjustedConfig.attackInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -167,8 +292,14 @@ export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: num
|
||||
return false
|
||||
}
|
||||
|
||||
// 有侦查报告的情况下,敌对NPC一定会攻击(移除概率限制)
|
||||
// 这样保证侦查后会跟进攻击,而不是无意义地反复侦查
|
||||
// 根据 AI 类型的攻击概率决定是否攻击
|
||||
// 侵略型总是攻击,其他类型按概率
|
||||
if (aiModifier.attackProbabilityMultiplier < 1.0) {
|
||||
if (Math.random() > adjustedConfig.attackProbability) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -183,20 +314,32 @@ export const shouldNPCGiftPlayer = (npc: NPC, player: Player, currentTime: numbe
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查上次赠送时间
|
||||
// 获取 AI 行为修改器
|
||||
const aiModifier = getAIBehaviorModifier(npc.aiType)
|
||||
|
||||
// 侵略型 NPC 永远不送礼
|
||||
if (aiModifier.giftProbabilityMultiplier === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查上次赠送时间(商人型间隔更短)
|
||||
const lastGiftTime = (npc as any).lastGiftTime || 0
|
||||
if (currentTime - lastGiftTime < NPC_GIFT_CONFIG.CHECK_INTERVAL * 1000) {
|
||||
const giftInterval = NPC_GIFT_CONFIG.CHECK_INTERVAL / aiModifier.giftProbabilityMultiplier
|
||||
if (currentTime - lastGiftTime < giftInterval * 1000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查好感度 - 统一使用 npc.relations
|
||||
// 商人型 NPC 好感度门槛更低
|
||||
const relation = npc.relations?.[player.id]
|
||||
if (!relation || relation.reputation < NPC_GIFT_CONFIG.MIN_REPUTATION) {
|
||||
const minReputation = npc.aiType === NPCAIType.Trader ? NPC_GIFT_CONFIG.MIN_REPUTATION * 0.5 : NPC_GIFT_CONFIG.MIN_REPUTATION
|
||||
if (!relation || relation.reputation < minReputation) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 随机概率
|
||||
return Math.random() < NPC_GIFT_CONFIG.GIFT_PROBABILITY
|
||||
// 随机概率(根据 AI 类型调整)
|
||||
const giftProbability = NPC_GIFT_CONFIG.GIFT_PROBABILITY * aiModifier.giftProbabilityMultiplier
|
||||
return Math.random() < giftProbability
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1143,19 +1286,29 @@ export const shouldNPCRevenge = (npc: NPC, currentTime: number): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取 AI 行为修改器
|
||||
const aiModifier = getAIBehaviorModifier(npc.aiType)
|
||||
|
||||
// 商人型 NPC 反击意愿很低,可能选择不反击
|
||||
if (aiModifier.revengeMultiplier < 1.0 && Math.random() > aiModifier.revengeMultiplier) {
|
||||
return false
|
||||
}
|
||||
|
||||
const attackRecord = npc.attackedBy[npc.revengeTarget]
|
||||
if (!attackRecord) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 被攻击后24小时内可以反击
|
||||
// 被攻击后24小时内可以反击(防守型可能更长时间记仇)
|
||||
const revengeWindow = 24 * 3600 * 1000 * aiModifier.revengeMultiplier
|
||||
const timeSinceLastAttack = currentTime - attackRecord.lastAttackTime
|
||||
if (timeSinceLastAttack > 24 * 3600 * 1000) {
|
||||
if (timeSinceLastAttack > revengeWindow) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 至少等待10分钟后再反击(给NPC时间准备)
|
||||
if (timeSinceLastAttack < 600 * 1000) {
|
||||
// 至少等待一段时间后再反击(防守型反应更快)
|
||||
const minWaitTime = aiModifier.defenseFocus ? 300 * 1000 : 600 * 1000 // 防守型5分钟,其他10分钟
|
||||
if (timeSinceLastAttack < minWaitTime) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1187,10 +1340,15 @@ export const createNPCRevengeMission = (npc: NPC, allPlanets: Planet[], config:
|
||||
return null
|
||||
}
|
||||
|
||||
// 反击时派出更多舰队(比正常攻击多50%)
|
||||
// 获取 AI 行为修改器
|
||||
const aiModifier = getAIBehaviorModifier(npc.aiType)
|
||||
|
||||
// 反击时派出更多舰队(基础多50%,根据 AI 类型调整)
|
||||
// 防守型/侵略型反击更猛烈
|
||||
const revengeMultiplier = 1.5 * aiModifier.revengeMultiplier
|
||||
const revengeFleet = decideAttackFleet(npc, npcPlanet, {} as SpyReport, {
|
||||
...config,
|
||||
attackFleetSizeRatio: Math.min(1.0, config.attackFleetSizeRatio * 1.5)
|
||||
attackFleetSizeRatio: Math.min(1.0, config.attackFleetSizeRatio * revengeMultiplier)
|
||||
})
|
||||
|
||||
if (!revengeFleet) {
|
||||
@@ -1257,6 +1415,7 @@ export interface NPCDiagnosticInfo {
|
||||
npcId: string
|
||||
npcName: string
|
||||
difficulty: string
|
||||
aiType?: NPCAIType // AI 行为类型
|
||||
relationStatus: string // 保持原有字段用于显示
|
||||
relationStatusKey: RelationStatusKey // 翻译键
|
||||
reputation: number
|
||||
@@ -1374,6 +1533,7 @@ export const diagnoseNPCBehavior = (npcs: NPC[], player: Player, currentTime: nu
|
||||
npcId: npc.id,
|
||||
npcName: npc.name,
|
||||
difficulty: npc.difficulty,
|
||||
aiType: npc.aiType,
|
||||
relationStatus,
|
||||
relationStatusKey,
|
||||
reputation,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NPC, Planet, Player } from '@/types/game'
|
||||
import { TechnologyType, BuildingType, ShipType, DefenseType } from '@/types/game'
|
||||
import { TechnologyType, BuildingType, ShipType, DefenseType, NPCAIType } from '@/types/game'
|
||||
import { BUILDINGS, SHIPS, TECHNOLOGIES } from '@/config/gameConfig'
|
||||
import * as buildingLogic from './buildingLogic'
|
||||
import * as researchLogic from './researchLogic'
|
||||
@@ -723,6 +723,40 @@ export const calculateDistanceDifficultyMultiplier = (distance: number): Distanc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成 NPC 的 AI 类型
|
||||
* 分布概率:
|
||||
* - Balanced: 35% (平衡型最常见)
|
||||
* - Aggressive: 25% (侵略型较常见)
|
||||
* - Defensive: 20% (防守型中等)
|
||||
* - Expansionist: 15% (扩张型较少)
|
||||
* - Trader: 5% (商人型最稀少)
|
||||
*/
|
||||
export const generateRandomAIType = (): NPCAIType => {
|
||||
const roll = Math.random() * 100
|
||||
if (roll < 35) return NPCAIType.Balanced
|
||||
if (roll < 60) return NPCAIType.Aggressive
|
||||
if (roll < 80) return NPCAIType.Defensive
|
||||
if (roll < 95) return NPCAIType.Expansionist
|
||||
return NPCAIType.Trader
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 NPC 有 AI 类型,如果没有则随机分配
|
||||
*/
|
||||
export const ensureNPCAIType = (npc: NPC): void => {
|
||||
if (!npc.aiType) {
|
||||
npc.aiType = generateRandomAIType()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保所有 NPC 都有 AI 类型
|
||||
*/
|
||||
export const ensureAllNPCsAIType = (npcs: NPC[]): void => {
|
||||
npcs.forEach(npc => ensureNPCAIType(npc))
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于距离难度初始化NPC星球
|
||||
* 替代旧的 initializeNPCStartingPower
|
||||
@@ -732,10 +766,7 @@ export const calculateDistanceDifficultyMultiplier = (distance: number): Distanc
|
||||
* 资源上限:基于仓储建筑等级计算 (10000 * 2^level)
|
||||
* 舰队数量:基于船坞等级和难度等级合理计算
|
||||
*/
|
||||
export const initializeNPCByDistance = (
|
||||
npc: NPC,
|
||||
homeworldPosition: { galaxy: number; system: number; position: number }
|
||||
): void => {
|
||||
export const initializeNPCByDistance = (npc: NPC, homeworldPosition: { galaxy: number; system: number; position: number }): void => {
|
||||
const planet = npc.planets[0]
|
||||
if (!planet) return
|
||||
|
||||
@@ -746,6 +777,9 @@ export const initializeNPCByDistance = (
|
||||
npc.distanceToHomeworld = distance
|
||||
npc.difficultyLevel = calculateDifficultyLevel(distance)
|
||||
|
||||
// 分配随机 AI 类型(如果还没有)
|
||||
ensureNPCAIType(npc)
|
||||
|
||||
// 基础等级 * 倍率,并限制上限
|
||||
const baseLevel = 5
|
||||
const MAX_BUILDING_LEVEL = 30
|
||||
@@ -797,11 +831,11 @@ export const initializeNPCByDistance = (
|
||||
// 分配舰队比例
|
||||
planet.fleet[ShipType.EspionageProbe] = Math.max(5, Math.floor(baseFleetCount * 0.05))
|
||||
planet.fleet[ShipType.LightFighter] = Math.floor(baseFleetCount * 0.35)
|
||||
planet.fleet[ShipType.HeavyFighter] = Math.floor(baseFleetCount * 0.20)
|
||||
planet.fleet[ShipType.HeavyFighter] = Math.floor(baseFleetCount * 0.2)
|
||||
planet.fleet[ShipType.Cruiser] = Math.floor(baseFleetCount * 0.15)
|
||||
planet.fleet[ShipType.Battleship] = Math.floor(baseFleetCount * 0.05)
|
||||
planet.fleet[ShipType.SmallCargo] = Math.floor(baseFleetCount * 0.10)
|
||||
planet.fleet[ShipType.Recycler] = Math.floor(baseFleetCount * 0.10)
|
||||
planet.fleet[ShipType.SmallCargo] = Math.floor(baseFleetCount * 0.1)
|
||||
planet.fleet[ShipType.Recycler] = Math.floor(baseFleetCount * 0.1)
|
||||
|
||||
// 设置防御设施(基于难度等级,合理范围)
|
||||
const defenseScale = difficultyLevel * 5
|
||||
|
||||
766
src/logic/npcNameGenerator.ts
Normal file
766
src/logic/npcNameGenerator.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
/**
|
||||
* NPC 名字生成器
|
||||
* 为 NPC 生成有代入感的随机名字,支持多语言
|
||||
*/
|
||||
|
||||
export type SupportedLocale = 'zh-CN' | 'zh-TW' | 'en' | 'de' | 'ru' | 'ko' | 'ja'
|
||||
|
||||
// 各语言的名字数据
|
||||
const nameData: Record<
|
||||
SupportedLocale,
|
||||
{
|
||||
prefixes: string[]
|
||||
firstNames: string[] // 对于中文是"姓"
|
||||
lastNames: string[] // 对于中文是"名"
|
||||
}
|
||||
> = {
|
||||
// 简体中文 - 采用"姓+名"格式,可选头衔
|
||||
'zh-CN': {
|
||||
prefixes: [
|
||||
'将军',
|
||||
'统帅',
|
||||
'元帅',
|
||||
'司令',
|
||||
'总督',
|
||||
'领主',
|
||||
'公爵',
|
||||
'帝王',
|
||||
'大师',
|
||||
'长老',
|
||||
'圣者',
|
||||
'先知',
|
||||
'星主',
|
||||
'舰长',
|
||||
'指挥官',
|
||||
'督军'
|
||||
],
|
||||
firstNames: [
|
||||
// 中文姓氏
|
||||
'云',
|
||||
'星',
|
||||
'龙',
|
||||
'凤',
|
||||
'墨',
|
||||
'风',
|
||||
'雷',
|
||||
'玄',
|
||||
'青',
|
||||
'白',
|
||||
'朱',
|
||||
'银',
|
||||
'暗',
|
||||
'烈',
|
||||
'寒',
|
||||
'钢',
|
||||
'铁',
|
||||
'战',
|
||||
'天',
|
||||
'皇',
|
||||
'帝',
|
||||
'王',
|
||||
'霸',
|
||||
'炎',
|
||||
'冰',
|
||||
'雪',
|
||||
'影',
|
||||
'光',
|
||||
'夜',
|
||||
'苍'
|
||||
],
|
||||
lastNames: [
|
||||
// 中文名
|
||||
'天翔',
|
||||
'云龙',
|
||||
'星河',
|
||||
'凌风',
|
||||
'剑心',
|
||||
'寒渊',
|
||||
'霆威',
|
||||
'狼牙',
|
||||
'武玄',
|
||||
'龙吟',
|
||||
'虎啸',
|
||||
'鹏飞',
|
||||
'辰耀',
|
||||
'焰灵',
|
||||
'霜羽',
|
||||
'电鸣',
|
||||
'涛声',
|
||||
'壁坚',
|
||||
'魂铸',
|
||||
'晓破',
|
||||
'暮辉',
|
||||
'极光',
|
||||
'彗尾',
|
||||
'傲天',
|
||||
'战神',
|
||||
'无双',
|
||||
'霸天',
|
||||
'九霄',
|
||||
'破军',
|
||||
'七杀'
|
||||
]
|
||||
},
|
||||
|
||||
// 繁体中文 - 採用"姓+名"格式,可選頭銜
|
||||
'zh-TW': {
|
||||
prefixes: [
|
||||
'將軍',
|
||||
'統帥',
|
||||
'元帥',
|
||||
'司令',
|
||||
'總督',
|
||||
'領主',
|
||||
'公爵',
|
||||
'帝王',
|
||||
'大師',
|
||||
'長老',
|
||||
'聖者',
|
||||
'先知',
|
||||
'星主',
|
||||
'艦長',
|
||||
'指揮官',
|
||||
'督軍'
|
||||
],
|
||||
firstNames: [
|
||||
// 中文姓氏
|
||||
'雲',
|
||||
'星',
|
||||
'龍',
|
||||
'鳳',
|
||||
'墨',
|
||||
'風',
|
||||
'雷',
|
||||
'玄',
|
||||
'青',
|
||||
'白',
|
||||
'朱',
|
||||
'銀',
|
||||
'暗',
|
||||
'烈',
|
||||
'寒',
|
||||
'鋼',
|
||||
'鐵',
|
||||
'戰',
|
||||
'天',
|
||||
'皇',
|
||||
'帝',
|
||||
'王',
|
||||
'霸',
|
||||
'炎',
|
||||
'冰',
|
||||
'雪',
|
||||
'影',
|
||||
'光',
|
||||
'夜',
|
||||
'蒼'
|
||||
],
|
||||
lastNames: [
|
||||
// 中文名
|
||||
'天翔',
|
||||
'雲龍',
|
||||
'星河',
|
||||
'凌風',
|
||||
'劍心',
|
||||
'寒淵',
|
||||
'霆威',
|
||||
'狼牙',
|
||||
'武玄',
|
||||
'龍吟',
|
||||
'虎嘯',
|
||||
'鵬飛',
|
||||
'辰耀',
|
||||
'焰靈',
|
||||
'霜羽',
|
||||
'電鳴',
|
||||
'濤聲',
|
||||
'壁堅',
|
||||
'魂鑄',
|
||||
'曉破',
|
||||
'暮輝',
|
||||
'極光',
|
||||
'彗尾',
|
||||
'傲天',
|
||||
'戰神',
|
||||
'無雙',
|
||||
'霸天',
|
||||
'九霄',
|
||||
'破軍',
|
||||
'七殺'
|
||||
]
|
||||
},
|
||||
|
||||
// 英语 - 科幻/西方风格
|
||||
en: {
|
||||
prefixes: [
|
||||
'Admiral',
|
||||
'Commander',
|
||||
'Captain',
|
||||
'General',
|
||||
'Marshal',
|
||||
'Warlord',
|
||||
'Lord',
|
||||
'Duke',
|
||||
'Baron',
|
||||
'Emperor',
|
||||
'King',
|
||||
'Prince',
|
||||
'Dr.',
|
||||
'Master',
|
||||
'Elder',
|
||||
'Oracle',
|
||||
'Archon',
|
||||
'Overseer',
|
||||
'Sentinel',
|
||||
'Guardian'
|
||||
],
|
||||
firstNames: [
|
||||
'Maximus',
|
||||
'Aurelius',
|
||||
'Tiberius',
|
||||
'Corvus',
|
||||
'Theron',
|
||||
'Darius',
|
||||
'Cyrus',
|
||||
'Orion',
|
||||
'Atlas',
|
||||
'Phoenix',
|
||||
'Ragnar',
|
||||
'Magnus',
|
||||
'Zarak',
|
||||
'Kael',
|
||||
'Vorn',
|
||||
'Xander',
|
||||
'Drax',
|
||||
'Raven',
|
||||
'Storm',
|
||||
'Nova',
|
||||
'Vector',
|
||||
'Reaper',
|
||||
'Vortex',
|
||||
'Aldaris',
|
||||
'Zeratul',
|
||||
'Artanis',
|
||||
'Arcturus',
|
||||
'Valerian',
|
||||
'Raynor',
|
||||
'Fenix'
|
||||
],
|
||||
lastNames: [
|
||||
'Darkblade',
|
||||
'Ironforge',
|
||||
'Stormwind',
|
||||
'Blackwood',
|
||||
'Stargazer',
|
||||
'Voidwalker',
|
||||
'Skybreaker',
|
||||
'Nebula',
|
||||
'Ironside',
|
||||
'Steelheart',
|
||||
'the Wise',
|
||||
'the Bold',
|
||||
'the Merciless',
|
||||
'the Conqueror',
|
||||
'the Destroyer',
|
||||
'the Eternal'
|
||||
]
|
||||
},
|
||||
|
||||
// 德语 - 日耳曼/科幻风格
|
||||
de: {
|
||||
prefixes: [
|
||||
'Admiral',
|
||||
'Kommandant',
|
||||
'Kapitän',
|
||||
'General',
|
||||
'Marschall',
|
||||
'Kriegsherr',
|
||||
'Fürst',
|
||||
'Herzog',
|
||||
'Baron',
|
||||
'Kaiser',
|
||||
'König',
|
||||
'Prinz',
|
||||
'Meister',
|
||||
'Ältester',
|
||||
'Hüter',
|
||||
'Wächter',
|
||||
'Richter',
|
||||
'Vollstrecker'
|
||||
],
|
||||
firstNames: [
|
||||
'Wolfgang',
|
||||
'Friedrich',
|
||||
'Heinrich',
|
||||
'Siegfried',
|
||||
'Dietrich',
|
||||
'Adalbert',
|
||||
'Konrad',
|
||||
'Gerhard',
|
||||
'Reinhard',
|
||||
'Lothar',
|
||||
'Gunther',
|
||||
'Alaric',
|
||||
'Baldur',
|
||||
'Ragnar',
|
||||
'Thorvald',
|
||||
'Sigurd',
|
||||
'Harald',
|
||||
'Bjorn',
|
||||
'Fenris',
|
||||
'Grimwald',
|
||||
'Steinhart',
|
||||
'Eisenherz',
|
||||
'Donner',
|
||||
'Blitz',
|
||||
'Sturm',
|
||||
'Schatten',
|
||||
'Flamme',
|
||||
'Frost'
|
||||
],
|
||||
lastNames: [
|
||||
'von Kriegstein',
|
||||
'von Donnerfels',
|
||||
'von Eisenberg',
|
||||
'von Schwarzwald',
|
||||
'Sturmbrecher',
|
||||
'Sternenkrieger',
|
||||
'Schattenläufer',
|
||||
'der Weise',
|
||||
'der Tapfere',
|
||||
'der Gnadenlose',
|
||||
'der Eroberer',
|
||||
'der Zerstörer',
|
||||
'der Ewige',
|
||||
'der Furchtlose',
|
||||
'der Mächtige'
|
||||
]
|
||||
},
|
||||
|
||||
// 俄语 - 斯拉夫/科幻风格
|
||||
ru: {
|
||||
prefixes: [
|
||||
'Адмирал',
|
||||
'Командир',
|
||||
'Капитан',
|
||||
'Генерал',
|
||||
'Маршал',
|
||||
'Воевода',
|
||||
'Князь',
|
||||
'Герцог',
|
||||
'Барон',
|
||||
'Царь',
|
||||
'Император',
|
||||
'Владыка',
|
||||
'Мастер',
|
||||
'Старейшина',
|
||||
'Страж',
|
||||
'Хранитель',
|
||||
'Судья',
|
||||
'Каратель'
|
||||
],
|
||||
firstNames: [
|
||||
'Владимир',
|
||||
'Александр',
|
||||
'Дмитрий',
|
||||
'Николай',
|
||||
'Сергей',
|
||||
'Андрей',
|
||||
'Михаил',
|
||||
'Игорь',
|
||||
'Ярослав',
|
||||
'Святослав',
|
||||
'Борис',
|
||||
'Олег',
|
||||
'Руслан',
|
||||
'Богдан',
|
||||
'Вадим',
|
||||
'Громовой',
|
||||
'Молния',
|
||||
'Буря',
|
||||
'Тень',
|
||||
'Пламя',
|
||||
'Мороз',
|
||||
'Сталь',
|
||||
'Железо',
|
||||
'Космос',
|
||||
'Звезда',
|
||||
'Комета',
|
||||
'Туман',
|
||||
'Вихрь'
|
||||
],
|
||||
lastNames: [
|
||||
'Громобой',
|
||||
'Железнов',
|
||||
'Стальной',
|
||||
'Черный',
|
||||
'Звездочет',
|
||||
'Пустоход',
|
||||
'Небоход',
|
||||
'Мудрый',
|
||||
'Храбрый',
|
||||
'Беспощадный',
|
||||
'Завоеватель',
|
||||
'Разрушитель',
|
||||
'Вечный',
|
||||
'Бесстрашный',
|
||||
'Могучий',
|
||||
'Непобедимый'
|
||||
]
|
||||
},
|
||||
|
||||
// 韩语 - 韩国风格,采用"姓+名"格式
|
||||
ko: {
|
||||
prefixes: ['제독', '사령관', '함장', '장군', '원수', '군주', '영주', '황제', '왕', '장로', '현자', '수호자', '지휘관', '총독'],
|
||||
firstNames: [
|
||||
// 韩国姓氏
|
||||
'김',
|
||||
'이',
|
||||
'박',
|
||||
'최',
|
||||
'정',
|
||||
'강',
|
||||
'조',
|
||||
'윤',
|
||||
'장',
|
||||
'임',
|
||||
'한',
|
||||
'오',
|
||||
'서',
|
||||
'신',
|
||||
'권',
|
||||
'황',
|
||||
'안',
|
||||
'송',
|
||||
'류',
|
||||
'홍'
|
||||
],
|
||||
lastNames: [
|
||||
// 韩国名字(带有科幻/古风感)
|
||||
'천룡',
|
||||
'성하',
|
||||
'용현',
|
||||
'검심',
|
||||
'풍화',
|
||||
'뇌정',
|
||||
'천랑',
|
||||
'현무',
|
||||
'은하',
|
||||
'성진',
|
||||
'광년',
|
||||
'열염',
|
||||
'뇌전',
|
||||
'광풍',
|
||||
'철벽',
|
||||
'강혼',
|
||||
'극광',
|
||||
'태양',
|
||||
'별빛',
|
||||
'우주'
|
||||
]
|
||||
},
|
||||
|
||||
// 日语 - 日本风格,采用"姓+名"格式
|
||||
ja: {
|
||||
prefixes: ['提督', '司令官', '艦長', '将軍', '元帥', '領主', '皇帝', '王', '師匠', '長老', '賢者', '守護者', '指揮官', '総督'],
|
||||
firstNames: [
|
||||
// 日本姓氏
|
||||
'山本',
|
||||
'田中',
|
||||
'佐藤',
|
||||
'鈴木',
|
||||
'高橋',
|
||||
'伊藤',
|
||||
'渡辺',
|
||||
'中村',
|
||||
'小林',
|
||||
'加藤',
|
||||
'吉田',
|
||||
'山田',
|
||||
'松本',
|
||||
'井上',
|
||||
'木村',
|
||||
'林',
|
||||
'斎藤',
|
||||
'清水',
|
||||
'山口',
|
||||
'阿部'
|
||||
],
|
||||
lastNames: [
|
||||
// 日本名字(带有科幻/古风感)
|
||||
'龍馬',
|
||||
'星河',
|
||||
'雷神',
|
||||
'風神',
|
||||
'武蔵',
|
||||
'銀河',
|
||||
'彗星',
|
||||
'流星',
|
||||
'閃光',
|
||||
'烈火',
|
||||
'氷雪',
|
||||
'雷電',
|
||||
'疾風',
|
||||
'鋼鉄',
|
||||
'黎明',
|
||||
'黄昏',
|
||||
'光輝',
|
||||
'暁',
|
||||
'蒼天',
|
||||
'紅蓮'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 id 作为种子的伪随机数生成器
|
||||
const seededRandom = (seed: string): (() => number) => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
const char = seed.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash = hash & hash
|
||||
}
|
||||
|
||||
return () => {
|
||||
hash = (hash * 1103515245 + 12345) & 0x7fffffff
|
||||
return hash / 0x7fffffff
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据语言代码获取对应的名字数据
|
||||
*/
|
||||
const getNameDataForLocale = (locale: string): (typeof nameData)[SupportedLocale] => {
|
||||
// 标准化语言代码
|
||||
const normalizedLocale = locale.toLowerCase()
|
||||
|
||||
if (normalizedLocale.startsWith('zh-tw') || normalizedLocale.startsWith('zh-hant')) {
|
||||
return nameData['zh-TW']
|
||||
}
|
||||
if (normalizedLocale.startsWith('zh')) {
|
||||
return nameData['zh-CN']
|
||||
}
|
||||
if (normalizedLocale.startsWith('de')) {
|
||||
return nameData['de']
|
||||
}
|
||||
if (normalizedLocale.startsWith('ru')) {
|
||||
return nameData['ru']
|
||||
}
|
||||
if (normalizedLocale.startsWith('ko')) {
|
||||
return nameData['ko']
|
||||
}
|
||||
if (normalizedLocale.startsWith('ja')) {
|
||||
return nameData['ja']
|
||||
}
|
||||
|
||||
// 默认英语
|
||||
return nameData['en']
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是东亚语言(中文、日文、韩文)
|
||||
* 这些语言的名字不使用空格分隔
|
||||
*/
|
||||
const isEastAsianLocale = (locale: string): boolean => {
|
||||
const normalized = locale.toLowerCase()
|
||||
return normalized.startsWith('zh') || normalized.startsWith('ja') || normalized.startsWith('ko')
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 NPC 名字
|
||||
* @param npcId NPC 的唯一 ID,用作随机种子确保同一 NPC 名字一致
|
||||
* @param locale 语言代码,如 'zh-CN', 'en', 'de' 等
|
||||
* @param options 可选配置
|
||||
* @returns 生成的名字
|
||||
*/
|
||||
export const generateNPCName = (
|
||||
npcId: string,
|
||||
locale: string = 'en',
|
||||
options?: {
|
||||
includePrefix?: boolean // 是否包含头衔前缀
|
||||
includeLastName?: boolean // 是否包含姓氏
|
||||
style?: 'simple' | 'full' | 'titled' // 名字风格
|
||||
}
|
||||
): string => {
|
||||
const random = seededRandom(npcId)
|
||||
const data = getNameDataForLocale(locale)
|
||||
const isEastAsian = isEastAsianLocale(locale)
|
||||
|
||||
const style = options?.style ?? 'full'
|
||||
const includePrefix = options?.includePrefix ?? style === 'titled'
|
||||
|
||||
// 头衔前缀 (30% 概率,或指定包含)
|
||||
let prefix = ''
|
||||
if (includePrefix || random() < 0.3) {
|
||||
const prefixIndex = Math.floor(random() * data.prefixes.length)
|
||||
prefix = data.prefixes[prefixIndex]!
|
||||
}
|
||||
|
||||
// 名字主体 (必须) - 对于中文是"姓"
|
||||
const firstNameIndex = Math.floor(random() * data.firstNames.length)
|
||||
const firstName = data.firstNames[firstNameIndex]!
|
||||
|
||||
// 姓氏/名 (必须包含) - 对于中文是"名"
|
||||
const lastNameIndex = Math.floor(random() * data.lastNames.length)
|
||||
const lastName = data.lastNames[lastNameIndex]!
|
||||
|
||||
// 根据语言组合名字
|
||||
if (isEastAsian) {
|
||||
// 东亚语言:姓名不用空格,头衔与名字用空格分隔
|
||||
// 格式:[头衔] 姓名 例如:将军 云天翔
|
||||
const fullName = firstName + lastName
|
||||
return prefix ? `${prefix} ${fullName}` : fullName
|
||||
} else {
|
||||
// 西方语言:所有部分用空格分隔
|
||||
// 格式:[Title] FirstName LastName 例如:Admiral Orion Darkblade
|
||||
const parts: string[] = []
|
||||
if (prefix) parts.push(prefix)
|
||||
parts.push(firstName)
|
||||
parts.push(lastName)
|
||||
return parts.join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成不重复的 NPC 名字
|
||||
* @param count 需要生成的数量
|
||||
* @param locale 语言代码
|
||||
* @param existingNames 已存在的名字列表(避免重复)
|
||||
* @returns 名字数组
|
||||
*/
|
||||
export const generateUniqueNPCNames = (count: number, locale: string = 'en', existingNames: string[] = []): string[] => {
|
||||
const names: string[] = []
|
||||
const usedNames = new Set(existingNames)
|
||||
let attempts = 0
|
||||
const maxAttempts = count * 10
|
||||
|
||||
while (names.length < count && attempts < maxAttempts) {
|
||||
const seed = `npc-${Date.now()}-${attempts}-${Math.random()}`
|
||||
const name = generateNPCName(seed, locale)
|
||||
|
||||
if (!usedNames.has(name)) {
|
||||
names.push(name)
|
||||
usedNames.add(name)
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版英文名字的特征模式
|
||||
* 用于检测是否是旧格式的 NPC 名字
|
||||
*/
|
||||
const oldEnglishNamePatterns = [
|
||||
// 英文头衔
|
||||
/^(Admiral|Commander|Captain|General|Marshal|Warlord|Lord|Duke|Baron|Emperor|King|Prince|Dr\.|Master|Elder|Oracle|Archon|Overseer|Sentinel|Guardian)\s/i,
|
||||
// 英文名字特征(常见的科幻名字)
|
||||
/\b(Maximus|Aurelius|Tiberius|Corvus|Theron|Darius|Cyrus|Orion|Atlas|Phoenix|Ragnar|Magnus|Zarak|Kael|Vorn|Xander|Drax|Raven|Storm|Nova|Vector|Reaper|Vortex|Aldaris|Zeratul|Artanis|Arcturus|Valerian|Raynor|Fenix)\b/i,
|
||||
// 英文姓氏特征
|
||||
/\b(Darkblade|Ironforge|Stormwind|Blackwood|Stargazer|Voidwalker|Skybreaker|Nebula|Ironside|Steelheart|the Wise|the Bold|the Merciless|the Conqueror|the Destroyer|the Eternal)\b/i
|
||||
]
|
||||
|
||||
/**
|
||||
* 旧版中文名字的特征模式(之前的错误格式)
|
||||
* 例如:星辰 征服者、长老 暗影 执法者
|
||||
*/
|
||||
const oldChineseNamePatterns = [
|
||||
// 旧版后缀(之主、之王等直接作为名字的一部分)
|
||||
/\s(之主|之王|天尊|霸主|守护者|征服者|毁灭者|创世者|裁决者|先驱者|开拓者|统治者|复仇者|守望者|执法者|追猎者)$/,
|
||||
// 旧版繁体后缀
|
||||
/\s(之主|之王|天尊|霸主|守護者|征服者|毀滅者|創世者|裁決者|先驅者|開拓者|統治者|復仇者|守望者|執法者|追獵者)$/
|
||||
]
|
||||
|
||||
/**
|
||||
* 检测 NPC 名字是否是原始 ID 格式(如 NPC-npc_10)
|
||||
*/
|
||||
const isRawIdFormat = (name: string): boolean => {
|
||||
// 匹配 NPC-npc_XX 或 npc_XX 格式
|
||||
return /^(NPC-)?npc_\d+$/i.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 NPC 名字是否是旧格式
|
||||
* @param name NPC 当前的名字
|
||||
* @param locale 当前语言
|
||||
* @returns 是否是旧格式
|
||||
*/
|
||||
export const isOldFormatNPCName = (name: string, locale: string): boolean => {
|
||||
// 首先检测是否是原始 ID 格式(所有语言都需要更新)
|
||||
if (isRawIdFormat(name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const normalizedLocale = locale.toLowerCase()
|
||||
|
||||
// 对于中文用户,检测是否是英文名字或旧版中文格式
|
||||
if (normalizedLocale.startsWith('zh')) {
|
||||
// 检测英文名字模式
|
||||
for (const pattern of oldEnglishNamePatterns) {
|
||||
if (pattern.test(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 检测旧版中文格式
|
||||
for (const pattern of oldChineseNamePatterns) {
|
||||
if (pattern.test(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 对于日语用户,检测是否是英文名字
|
||||
if (normalizedLocale.startsWith('ja')) {
|
||||
for (const pattern of oldEnglishNamePatterns) {
|
||||
if (pattern.test(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 对于韩语用户,检测是否是英文名字
|
||||
if (normalizedLocale.startsWith('ko')) {
|
||||
for (const pattern of oldEnglishNamePatterns) {
|
||||
if (pattern.test(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 对于德语/俄语等西方语言用户,不需要特别检测
|
||||
// 因为英文名字格式与它们类似,不影响代入感
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 NPC 列表中是否有旧格式的名字
|
||||
* @param npcs NPC 列表
|
||||
* @param locale 当前语言
|
||||
* @returns 需要更新的 NPC 数量
|
||||
*/
|
||||
export const countOldFormatNPCs = (npcs: Array<{ id: string; name: string }>, locale: string): number => {
|
||||
return npcs.filter(npc => isOldFormatNPCName(npc.name, locale)).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 NPC 名字为新格式
|
||||
* @param npc NPC 对象
|
||||
* @param locale 当前语言
|
||||
* @returns 更新后的名字
|
||||
*/
|
||||
export const updateNPCName = (npcId: string, locale: string): string => {
|
||||
return generateNPCName(npcId, locale)
|
||||
}
|
||||
|
||||
export default {
|
||||
generateNPCName,
|
||||
generateUniqueNPCNames,
|
||||
isOldFormatNPCName,
|
||||
countOldFormatNPCs,
|
||||
updateNPCName,
|
||||
nameData
|
||||
}
|
||||
183
src/logic/oreDepositLogic.ts
Normal file
183
src/logic/oreDepositLogic.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 矿脉储量逻辑
|
||||
* 处理星球矿脉储量的生成、消耗和效率计算
|
||||
*/
|
||||
|
||||
import type { Planet, OreDeposits } from '@/types/game'
|
||||
import { ORE_DEPOSIT_CONFIG } from '@/config/gameConfig'
|
||||
|
||||
/**
|
||||
* 根据星球位置生成初始矿脉储量
|
||||
*/
|
||||
export const generateOreDeposits = (position: { galaxy: number; system: number; position: number }): OreDeposits => {
|
||||
const { BASE_DEPOSITS, POSITION_MULTIPLIERS, GALAXY_MULTIPLIER, RANDOM_VARIANCE } = ORE_DEPOSIT_CONFIG
|
||||
|
||||
// 位置索引 (0-14)
|
||||
const posIndex = Math.max(0, Math.min(14, position.position - 1))
|
||||
|
||||
// 银河系加成 (银河系1为基础,每增加1个银河系增加5%)
|
||||
const galaxyBonus = 1 + (position.galaxy - 1) * GALAXY_MULTIPLIER
|
||||
|
||||
// 计算每种资源的储量
|
||||
const calculateDeposit = (baseAmount: number, positionMultiplier: number): number => {
|
||||
// 基础储量 × 位置系数 × 银河系加成
|
||||
const baseDeposit = baseAmount * positionMultiplier * galaxyBonus
|
||||
|
||||
// 添加随机浮动 (±RANDOM_VARIANCE)
|
||||
const variance = 1 + (Math.random() * 2 - 1) * RANDOM_VARIANCE
|
||||
return Math.floor(baseDeposit * variance)
|
||||
}
|
||||
|
||||
const metalDeposit = calculateDeposit(BASE_DEPOSITS.metal, POSITION_MULTIPLIERS.metal[posIndex] ?? 1)
|
||||
const crystalDeposit = calculateDeposit(BASE_DEPOSITS.crystal, POSITION_MULTIPLIERS.crystal[posIndex] ?? 1)
|
||||
const deuteriumDeposit = calculateDeposit(BASE_DEPOSITS.deuterium, POSITION_MULTIPLIERS.deuterium[posIndex] ?? 1)
|
||||
|
||||
return {
|
||||
metal: metalDeposit,
|
||||
crystal: crystalDeposit,
|
||||
deuterium: deuteriumDeposit,
|
||||
initialMetal: metalDeposit,
|
||||
initialCrystal: crystalDeposit,
|
||||
initialDeuterium: deuteriumDeposit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算矿脉储量对产量的效率系数
|
||||
* 当储量低于衰减阈值时,产量会线性下降
|
||||
* @returns 0-1 之间的效率系数
|
||||
*/
|
||||
export const calculateDepositEfficiency = (deposits: OreDeposits | undefined, resourceType: 'metal' | 'crystal' | 'deuterium'): number => {
|
||||
if (!deposits) return 1 // 没有储量数据时返回满效率(向后兼容)
|
||||
|
||||
const { DECAY_START_THRESHOLD } = ORE_DEPOSIT_CONFIG
|
||||
|
||||
const currentDeposit = deposits[resourceType]
|
||||
const initialDeposit =
|
||||
resourceType === 'metal' ? deposits.initialMetal : resourceType === 'crystal' ? deposits.initialCrystal : deposits.initialDeuterium
|
||||
|
||||
// 如果初始储量为0,返回0(避免除以0)
|
||||
if (initialDeposit <= 0) return 0
|
||||
|
||||
// 计算剩余百分比
|
||||
const remainingPercentage = currentDeposit / initialDeposit
|
||||
|
||||
// 如果已耗尽,返回0
|
||||
if (currentDeposit <= 0) return 0
|
||||
|
||||
// 如果高于衰减阈值,返回满效率
|
||||
if (remainingPercentage >= DECAY_START_THRESHOLD) return 1
|
||||
|
||||
// 在衰减阈值以下,线性衰减
|
||||
// 从 DECAY_START_THRESHOLD 到 0,效率从 1 降到 0
|
||||
return remainingPercentage / DECAY_START_THRESHOLD
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗矿脉储量
|
||||
* @param deposits 矿脉储量对象
|
||||
* @param resourceType 资源类型
|
||||
* @param amount 要消耗的量
|
||||
* @returns 实际消耗的量(不会超过剩余储量)
|
||||
*/
|
||||
export const consumeDeposit = (deposits: OreDeposits, resourceType: 'metal' | 'crystal' | 'deuterium', amount: number): number => {
|
||||
const currentDeposit = deposits[resourceType]
|
||||
const actualConsumption = Math.min(currentDeposit, amount)
|
||||
|
||||
deposits[resourceType] = Math.max(0, currentDeposit - actualConsumption)
|
||||
|
||||
return actualConsumption
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取矿脉剩余百分比
|
||||
*/
|
||||
export const getDepositPercentage = (deposits: OreDeposits | undefined, resourceType: 'metal' | 'crystal' | 'deuterium'): number => {
|
||||
if (!deposits) return 100
|
||||
|
||||
const currentDeposit = deposits[resourceType]
|
||||
const initialDeposit =
|
||||
resourceType === 'metal' ? deposits.initialMetal : resourceType === 'crystal' ? deposits.initialCrystal : deposits.initialDeuterium
|
||||
|
||||
if (initialDeposit <= 0) return 0
|
||||
|
||||
return (currentDeposit / initialDeposit) * 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查矿脉是否处于警告状态(低于警告阈值)
|
||||
*/
|
||||
export const isDepositWarning = (deposits: OreDeposits | undefined, resourceType: 'metal' | 'crystal' | 'deuterium'): boolean => {
|
||||
if (!deposits) return false
|
||||
|
||||
const percentage = getDepositPercentage(deposits, resourceType)
|
||||
return percentage < ORE_DEPOSIT_CONFIG.WARNING_THRESHOLD * 100 && percentage > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查矿脉是否已耗尽
|
||||
*/
|
||||
export const isDepositDepleted = (deposits: OreDeposits | undefined, resourceType: 'metal' | 'crystal' | 'deuterium'): boolean => {
|
||||
if (!deposits) return false
|
||||
return deposits[resourceType] <= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 为现有星球迁移/初始化矿脉储量
|
||||
* 如果星球没有矿脉数据,则生成新的储量
|
||||
*/
|
||||
export const migrateOreDeposits = (planet: Planet): void => {
|
||||
// 月球不需要矿脉(没有采矿建筑)
|
||||
if (planet.isMoon) return
|
||||
|
||||
if (!planet.oreDeposits) {
|
||||
planet.oreDeposits = generateOreDeposits(planet.position)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算预计耗尽时间(小时)
|
||||
* @param deposits 矿脉储量
|
||||
* @param resourceType 资源类型
|
||||
* @param productionPerHour 每小时产量
|
||||
* @returns 预计耗尽时间(小时),如果产量为0则返回Infinity
|
||||
*/
|
||||
export const calculateDepletionTime = (
|
||||
deposits: OreDeposits | undefined,
|
||||
resourceType: 'metal' | 'crystal' | 'deuterium',
|
||||
productionPerHour: number
|
||||
): number => {
|
||||
if (!deposits || productionPerHour <= 0) return Infinity
|
||||
|
||||
const currentDeposit = deposits[resourceType]
|
||||
if (currentDeposit <= 0) return 0
|
||||
|
||||
return currentDeposit / productionPerHour
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化耗尽时间为可读字符串
|
||||
*/
|
||||
export const formatDepletionTime = (hours: number): string => {
|
||||
if (!isFinite(hours) || hours < 0) return '∞'
|
||||
if (hours === 0) return '0'
|
||||
|
||||
const days = Math.floor(hours / 24)
|
||||
const remainingHours = Math.floor(hours % 24)
|
||||
|
||||
if (days > 365) {
|
||||
const years = Math.floor(days / 365)
|
||||
return `${years}y+`
|
||||
}
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${remainingHours}h`
|
||||
}
|
||||
|
||||
if (hours < 1) {
|
||||
const minutes = Math.floor(hours * 60)
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
return `${remainingHours}h`
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Planet, Resources } from '@/types/game'
|
||||
import { ShipType, DefenseType, BuildingType } from '@/types/game'
|
||||
import { MOON_CONFIG, PLANET_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
|
||||
import * as oreDepositLogic from './oreDepositLogic'
|
||||
|
||||
/**
|
||||
* 创建初始星球
|
||||
@@ -50,6 +51,7 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
waitingBuildQueue: [], // 等待队列
|
||||
lastUpdate: Date.now(),
|
||||
maxSpace: 200,
|
||||
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
|
||||
@@ -61,6 +63,12 @@ export const createInitialPlanet = (playerId: string, planetName: string = 'Home
|
||||
initialPlanet.buildings[building] = 0
|
||||
})
|
||||
|
||||
// 初始化矿脉储量
|
||||
initialPlanet.oreDeposits = oreDepositLogic.generateOreDeposits(initialPlanet.position)
|
||||
|
||||
// 初始化温度
|
||||
initialPlanet.temperature = generatePlanetTemperature(initialPlanet.position.position)
|
||||
|
||||
return initialPlanet
|
||||
}
|
||||
|
||||
@@ -116,6 +124,7 @@ export const createNPCPlanet = (
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
waitingBuildQueue: [], // 等待队列
|
||||
lastUpdate: Date.now(),
|
||||
maxSpace: 200,
|
||||
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
|
||||
@@ -127,6 +136,12 @@ export const createNPCPlanet = (
|
||||
npcPlanet.buildings[building] = Math.floor(Math.random() * 10)
|
||||
})
|
||||
|
||||
// 初始化矿脉储量
|
||||
npcPlanet.oreDeposits = oreDepositLogic.generateOreDeposits(npcPlanet.position)
|
||||
|
||||
// 初始化温度
|
||||
npcPlanet.temperature = generatePlanetTemperature(npcPlanet.position.position)
|
||||
|
||||
return npcPlanet
|
||||
}
|
||||
|
||||
@@ -201,6 +216,7 @@ export const createMoon = (
|
||||
[DefenseType.PlanetaryShield]: 0
|
||||
},
|
||||
buildQueue: [],
|
||||
waitingBuildQueue: [], // 等待队列
|
||||
lastUpdate: Date.now(),
|
||||
maxSpace: MOON_CONFIG.baseFields, // OGame规则:月球初始只有1格空间
|
||||
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
|
||||
@@ -245,3 +261,60 @@ export const calculatePlanetMaxSpace = (planet: Planet, terraformingTechLevel: n
|
||||
|
||||
return maxSpace
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据星球位置生成温度范围
|
||||
* OGame 原版规则:位置1-3靠近恒星(高温),位置13-15远离恒星(低温)
|
||||
* 温度影响太阳能卫星产能和重氢合成器产量
|
||||
*
|
||||
* 位置1: +220°C ~ +260°C (极热)
|
||||
* 位置2: +180°C ~ +220°C
|
||||
* 位置3: +100°C ~ +140°C
|
||||
* 位置4: +60°C ~ +100°C
|
||||
* 位置5: +30°C ~ +70°C
|
||||
* 位置6: +10°C ~ +50°C
|
||||
* 位置7: -10°C ~ +30°C
|
||||
* 位置8: -30°C ~ +10°C (温和)
|
||||
* 位置9: -50°C ~ -10°C
|
||||
* 位置10: -70°C ~ -30°C
|
||||
* 位置11: -100°C ~ -60°C
|
||||
* 位置12: -130°C ~ -90°C
|
||||
* 位置13: -160°C ~ -120°C
|
||||
* 位置14: -190°C ~ -150°C
|
||||
* 位置15: -220°C ~ -180°C (极冷)
|
||||
*/
|
||||
export const generatePlanetTemperature = (position: number): { min: number; max: number } => {
|
||||
// 基础温度曲线:位置1最热,位置15最冷
|
||||
// 使用线性插值,从位置1的240°C到位置15的-200°C
|
||||
const baseTemp = 240 - (position - 1) * 31.4 // 每个位置降低约31.4°C
|
||||
|
||||
// 温度范围通常在40°C左右波动
|
||||
const variation = 20
|
||||
const randomOffset = Math.floor(Math.random() * variation * 2) - variation // -20 to +20
|
||||
|
||||
const maxTemp = Math.round(baseTemp + randomOffset)
|
||||
const minTemp = maxTemp - 40 // 最低温比最高温低40°C
|
||||
|
||||
return { min: minTemp, max: maxTemp }
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算太阳能卫星基于温度的能量产出
|
||||
* OGame 原版公式:(maxTemp + 160) / 6 (向下取整)
|
||||
* 温度越高,太阳能卫星产能越高
|
||||
*/
|
||||
export const calculateSolarSatelliteOutput = (maxTemperature: number): number => {
|
||||
// 确保最小产出为0,避免极冷星球产生负能量
|
||||
return Math.max(0, Math.floor((maxTemperature + 160) / 6))
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算重氢合成器基于温度的产量修正系数
|
||||
* OGame 原版规则:温度越低,重氢产量越高
|
||||
* 公式:1.36 - 0.004 * maxTemp (转换为百分比系数)
|
||||
* 在温度-40°C时产量最高(约156%),温度高时产量低
|
||||
*/
|
||||
export const calculateDeuteriumTemperatureBonus = (maxTemperature: number): number => {
|
||||
// 返回乘数,例如:-40°C时返回1.52,+100°C时返回0.96
|
||||
return 1.36 - 0.004 * maxTemperature
|
||||
}
|
||||
|
||||
213
src/logic/rankingLogic.ts
Normal file
213
src/logic/rankingLogic.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 排行榜逻辑模块
|
||||
* 计算玩家和NPC的各类积分,并生成排行榜
|
||||
*/
|
||||
import type { Player, NPC, Planet, RankingEntry, RankingCategory } from '@/types/game'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game'
|
||||
import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||
|
||||
/**
|
||||
* 计算建筑积分
|
||||
* 基于累计投入的资源计算(与侧边栏保持一致)
|
||||
*/
|
||||
export const calculateBuildingScore = (planets: Planet[]): number => {
|
||||
let totalCost = 0
|
||||
|
||||
for (const planet of planets) {
|
||||
for (const buildingType of Object.values(BuildingType)) {
|
||||
const level = planet.buildings[buildingType] || 0
|
||||
if (level <= 0) continue
|
||||
|
||||
const config = BUILDINGS[buildingType]
|
||||
if (!config) continue
|
||||
|
||||
// 计算从1级到当前等级的累计成本(每级单独取整,与 publicLogic 一致)
|
||||
for (let lvl = 1; lvl <= level; lvl++) {
|
||||
const multiplier = Math.pow(config.costMultiplier, lvl - 1)
|
||||
const metal = Math.floor(config.baseCost.metal * multiplier)
|
||||
const crystal = Math.floor(config.baseCost.crystal * multiplier)
|
||||
const deuterium = Math.floor(config.baseCost.deuterium * multiplier)
|
||||
totalCost += metal + crystal + deuterium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(totalCost / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算研究积分
|
||||
* 基于累计投入的资源计算(与侧边栏保持一致,不计入暗物质)
|
||||
*/
|
||||
export const calculateResearchScore = (technologies: Record<TechnologyType, number>): number => {
|
||||
let totalCost = 0
|
||||
|
||||
for (const techType of Object.values(TechnologyType)) {
|
||||
const level = technologies[techType] || 0
|
||||
if (level <= 0) continue
|
||||
|
||||
const config = TECHNOLOGIES[techType]
|
||||
if (!config) continue
|
||||
|
||||
// 计算从1级到当前等级的累计成本(每级单独取整,与 publicLogic 一致)
|
||||
for (let lvl = 1; lvl <= level; lvl++) {
|
||||
const multiplier = Math.pow(config.costMultiplier, lvl - 1)
|
||||
const metal = Math.floor(config.baseCost.metal * multiplier)
|
||||
const crystal = Math.floor(config.baseCost.crystal * multiplier)
|
||||
const deuterium = Math.floor(config.baseCost.deuterium * multiplier)
|
||||
// 不计入暗物质,与侧边栏保持一致
|
||||
totalCost += metal + crystal + deuterium
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(totalCost / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算舰队积分
|
||||
* 基于当前拥有的舰船计算(与侧边栏保持一致,不含飞行中舰队,不计入暗物质)
|
||||
*/
|
||||
export const calculateFleetScore = (planets: Planet[]): number => {
|
||||
let totalCost = 0
|
||||
|
||||
// 只计算停靠在星球上的舰队(与侧边栏保持一致)
|
||||
for (const planet of planets) {
|
||||
for (const shipType of Object.values(ShipType)) {
|
||||
const count = planet.fleet[shipType] || 0
|
||||
if (count <= 0) continue
|
||||
|
||||
const config = SHIPS[shipType]
|
||||
if (!config) continue
|
||||
|
||||
// 不计入暗物质,与侧边栏保持一致
|
||||
const cost = (config.cost.metal + config.cost.crystal + config.cost.deuterium) * count
|
||||
totalCost += cost
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(totalCost / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算防御积分
|
||||
* 基于当前拥有的防御设施计算(与侧边栏保持一致,不计入暗物质)
|
||||
*/
|
||||
export const calculateDefenseScore = (planets: Planet[]): number => {
|
||||
let totalCost = 0
|
||||
|
||||
for (const planet of planets) {
|
||||
for (const defenseType of Object.values(DefenseType)) {
|
||||
const count = planet.defense[defenseType] || 0
|
||||
if (count <= 0) continue
|
||||
|
||||
const config = DEFENSES[defenseType]
|
||||
if (!config) continue
|
||||
|
||||
// 不计入暗物质,与侧边栏保持一致
|
||||
const cost = (config.cost.metal + config.cost.crystal + config.cost.deuterium) * count
|
||||
totalCost += cost
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(totalCost / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算玩家的所有积分
|
||||
*/
|
||||
export const calculatePlayerScores = (player: Player): RankingEntry['scores'] => {
|
||||
const building = calculateBuildingScore(player.planets)
|
||||
const research = calculateResearchScore(player.technologies)
|
||||
const fleet = calculateFleetScore(player.planets)
|
||||
const defense = calculateDefenseScore(player.planets)
|
||||
const total = building + research + fleet + defense
|
||||
|
||||
return { total, building, research, fleet, defense }
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算NPC的所有积分
|
||||
*/
|
||||
export const calculateNPCScores = (npc: NPC): RankingEntry['scores'] => {
|
||||
const building = calculateBuildingScore(npc.planets)
|
||||
const research = calculateResearchScore(npc.technologies)
|
||||
const fleet = calculateFleetScore(npc.planets)
|
||||
const defense = calculateDefenseScore(npc.planets)
|
||||
const total = building + research + fleet + defense
|
||||
|
||||
return { total, building, research, fleet, defense }
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成排行榜条目
|
||||
*/
|
||||
export const createRankingEntry = (
|
||||
id: string,
|
||||
name: string,
|
||||
isPlayer: boolean,
|
||||
scores: RankingEntry['scores'],
|
||||
planetCount: number
|
||||
): RankingEntry => {
|
||||
return { id, name, isPlayer, scores, planetCount }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的排行榜
|
||||
* @param player 玩家数据
|
||||
* @param npcs NPC列表
|
||||
* @param category 排行榜类别
|
||||
* @returns 按指定类别积分排序的排行榜
|
||||
*/
|
||||
export const getRanking = (player: Player, npcs: NPC[], category: RankingCategory): RankingEntry[] => {
|
||||
const entries: RankingEntry[] = []
|
||||
|
||||
// 添加玩家
|
||||
const playerScores = calculatePlayerScores(player)
|
||||
entries.push(createRankingEntry(player.id, player.name || '玩家', true, playerScores, player.planets.length))
|
||||
|
||||
// 添加所有NPC
|
||||
for (const npc of npcs) {
|
||||
if (npc.planets.length === 0) continue // 跳过没有星球的NPC
|
||||
const npcScores = calculateNPCScores(npc)
|
||||
entries.push(createRankingEntry(npc.id, npc.name, false, npcScores, npc.planets.length))
|
||||
}
|
||||
|
||||
// 按指定类别的积分降序排序
|
||||
entries.sort((a, b) => b.scores[category] - a.scores[category])
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家在指定类别的排名
|
||||
* @param player 玩家数据
|
||||
* @param npcs NPC列表
|
||||
* @param category 排行榜类别
|
||||
* @returns 玩家排名(从1开始)
|
||||
*/
|
||||
export const getPlayerRank = (player: Player, npcs: NPC[], category: RankingCategory): number => {
|
||||
const ranking = getRanking(player, npcs, category)
|
||||
const playerIndex = ranking.findIndex(entry => entry.isPlayer)
|
||||
return playerIndex + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排行榜统计信息
|
||||
*/
|
||||
export const getRankingStats = (
|
||||
player: Player,
|
||||
npcs: NPC[]
|
||||
): {
|
||||
totalPlayers: number
|
||||
playerRanks: Record<RankingCategory, number>
|
||||
} => {
|
||||
const totalPlayers = 1 + npcs.filter(npc => npc.planets.length > 0).length
|
||||
const categories: RankingCategory[] = ['total', 'building', 'research', 'fleet', 'defense']
|
||||
|
||||
const playerRanks = {} as Record<RankingCategory, number>
|
||||
for (const category of categories) {
|
||||
playerRanks[category] = getPlayerRank(player, npcs, category)
|
||||
}
|
||||
|
||||
return { totalPlayers, playerRanks }
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import type { Planet, Resources, Officer } from '@/types/game'
|
||||
import { BuildingType, OfficerType } from '@/types/game'
|
||||
import * as officerLogic from './officerLogic'
|
||||
import { OFFICERS } from '@/config/gameConfig'
|
||||
import * as oreDepositLogic from './oreDepositLogic'
|
||||
import * as planetLogic from './planetLogic'
|
||||
|
||||
/**
|
||||
* 计算电量产出
|
||||
@@ -23,8 +25,12 @@ export const calculateEnergyProduction = (
|
||||
// 核聚变反应堆每级产出:150 * 1.15^等级(消耗重氢)
|
||||
const fusionReactorProduction = fusionReactorLevel * 150 * Math.pow(1.15, fusionReactorLevel)
|
||||
|
||||
// 太阳能卫星每个产出:50点能量
|
||||
const solarSatelliteProduction = solarSatelliteCount * 50
|
||||
// 太阳能卫星产出:基于星球温度计算
|
||||
// OGame 原版公式:(maxTemp + 160) / 6
|
||||
// 温度越高,太阳能卫星产能越高
|
||||
const maxTemp = planet.temperature?.max ?? 0
|
||||
const solarSatelliteOutputPerUnit = planetLogic.calculateSolarSatelliteOutput(maxTemp)
|
||||
const solarSatelliteProduction = solarSatelliteCount * solarSatelliteOutputPerUnit
|
||||
|
||||
return (solarPlantProduction + fusionReactorProduction + solarSatelliteProduction) * energyBonus
|
||||
}
|
||||
@@ -33,16 +39,56 @@ export const calculateEnergyProduction = (
|
||||
* 计算电量消耗
|
||||
*/
|
||||
export const calculateEnergyConsumption = (planet: Planet): number => {
|
||||
// 资源建筑消耗
|
||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
|
||||
|
||||
// 设施建筑消耗
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
const shipyardLevel = planet.buildings[BuildingType.Shipyard] || 0
|
||||
const researchLabLevel = planet.buildings[BuildingType.ResearchLab] || 0
|
||||
const missileSiloLevel = planet.buildings[BuildingType.MissileSilo] || 0
|
||||
const terraformerLevel = planet.buildings[BuildingType.Terraformer] || 0
|
||||
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||
|
||||
// 月球建筑消耗
|
||||
const sensorPhalanxLevel = planet.buildings[BuildingType.SensorPhalanx] || 0
|
||||
const jumpGateLevel = planet.buildings[BuildingType.JumpGate] || 0
|
||||
|
||||
// 矿场每级消耗:10 * 1.1^等级
|
||||
const metalConsumption = metalMineLevel * 10 * Math.pow(1.1, metalMineLevel)
|
||||
const crystalConsumption = crystalMineLevel * 10 * Math.pow(1.1, crystalMineLevel)
|
||||
const deuteriumConsumption = deuteriumSynthesizerLevel * 15 * Math.pow(1.1, deuteriumSynthesizerLevel)
|
||||
|
||||
return metalConsumption + crystalConsumption + deuteriumConsumption
|
||||
// 设施建筑消耗
|
||||
const roboticsConsumption = roboticsFactoryLevel * 5 * Math.pow(1.1, roboticsFactoryLevel)
|
||||
const naniteConsumption = naniteFactoryLevel * 20 * Math.pow(1.15, naniteFactoryLevel)
|
||||
const shipyardConsumption = shipyardLevel * 8 * Math.pow(1.1, shipyardLevel)
|
||||
const researchLabConsumption = researchLabLevel * 12 * Math.pow(1.1, researchLabLevel)
|
||||
const missileSiloConsumption = missileSiloLevel * 8 * Math.pow(1.1, missileSiloLevel)
|
||||
const terraformerConsumption = terraformerLevel * 25 * Math.pow(1.15, terraformerLevel)
|
||||
const darkMatterCollectorConsumption = darkMatterCollectorLevel * 10 * Math.pow(1.1, darkMatterCollectorLevel)
|
||||
|
||||
// 月球建筑消耗
|
||||
const sensorPhalanxConsumption = sensorPhalanxLevel * 15 * Math.pow(1.12, sensorPhalanxLevel)
|
||||
const jumpGateConsumption = jumpGateLevel * 50 * Math.pow(1.2, jumpGateLevel)
|
||||
|
||||
return (
|
||||
metalConsumption +
|
||||
crystalConsumption +
|
||||
deuteriumConsumption +
|
||||
roboticsConsumption +
|
||||
naniteConsumption +
|
||||
shipyardConsumption +
|
||||
researchLabConsumption +
|
||||
missileSiloConsumption +
|
||||
terraformerConsumption +
|
||||
darkMatterCollectorConsumption +
|
||||
sensorPhalanxConsumption +
|
||||
jumpGateConsumption
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,10 +122,25 @@ export const calculateResourceProduction = (
|
||||
const hasPositiveEnergyBalance = energyProduction >= energyConsumption
|
||||
const productionEfficiency = hasPositiveEnergyBalance ? 1 : 0
|
||||
|
||||
// 计算矿脉储量效率(当储量接近耗尽时产量下降)
|
||||
const metalDepositEfficiency = oreDepositLogic.calculateDepositEfficiency(planet.oreDeposits, 'metal')
|
||||
const crystalDepositEfficiency = oreDepositLogic.calculateDepositEfficiency(planet.oreDeposits, 'crystal')
|
||||
const deuteriumDepositEfficiency = oreDepositLogic.calculateDepositEfficiency(planet.oreDeposits, 'deuterium')
|
||||
|
||||
// 重氢温度加成:温度越低,产量越高
|
||||
const deuteriumTempBonus = planetLogic.calculateDeuteriumTemperatureBonus(planet.temperature?.max ?? 0)
|
||||
|
||||
return {
|
||||
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
||||
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
|
||||
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
|
||||
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency * metalDepositEfficiency,
|
||||
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency * crystalDepositEfficiency,
|
||||
deuterium:
|
||||
deuteriumSynthesizerLevel *
|
||||
500 *
|
||||
Math.pow(1.5, deuteriumSynthesizerLevel) *
|
||||
resourceBonus *
|
||||
productionEfficiency *
|
||||
deuteriumDepositEfficiency *
|
||||
deuteriumTempBonus,
|
||||
darkMatter: darkMatterCollectorLevel * 100 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
|
||||
energy: energyProduction
|
||||
}
|
||||
@@ -146,17 +207,29 @@ export const updatePlanetResources = (
|
||||
// 能量不能为负数,最低为0
|
||||
planet.resources.energy = Math.max(0, planet.resources.energy)
|
||||
|
||||
// 计算资源产量(会检查能量是否充足)
|
||||
// 计算资源产量(会检查能量是否充足,以及矿脉储量效率)
|
||||
const production = calculateResourceProduction(planet, {
|
||||
resourceProductionBonus: bonuses.resourceProductionBonus,
|
||||
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
|
||||
energyProductionBonus: bonuses.energyProductionBonus
|
||||
})
|
||||
|
||||
// 计算实际产出量(用于消耗矿脉储量)
|
||||
const metalProduced = (production.metal * effectiveTimeDiff) / 3600
|
||||
const crystalProduced = (production.crystal * effectiveTimeDiff) / 3600
|
||||
const deuteriumProduced = (production.deuterium * effectiveTimeDiff) / 3600
|
||||
|
||||
// 消耗矿脉储量(如果有)
|
||||
if (planet.oreDeposits && !planet.isMoon) {
|
||||
oreDepositLogic.consumeDeposit(planet.oreDeposits, 'metal', metalProduced)
|
||||
oreDepositLogic.consumeDeposit(planet.oreDeposits, 'crystal', crystalProduced)
|
||||
oreDepositLogic.consumeDeposit(planet.oreDeposits, 'deuterium', deuteriumProduced)
|
||||
}
|
||||
|
||||
// 更新资源(转换为每秒产量,应用游戏速度)
|
||||
planet.resources.metal += (production.metal * effectiveTimeDiff) / 3600
|
||||
planet.resources.crystal += (production.crystal * effectiveTimeDiff) / 3600
|
||||
planet.resources.deuterium += (production.deuterium * effectiveTimeDiff) / 3600
|
||||
planet.resources.metal += metalProduced
|
||||
planet.resources.crystal += crystalProduced
|
||||
planet.resources.deuterium += deuteriumProduced
|
||||
planet.resources.darkMatter += (production.darkMatter * effectiveTimeDiff) / 3600
|
||||
|
||||
// 限制资源上限
|
||||
@@ -275,9 +348,21 @@ export interface ProductionBonus {
|
||||
* 能量消耗详细信息
|
||||
*/
|
||||
export interface ConsumptionBreakdown {
|
||||
// 资源建筑
|
||||
metalMine: ConsumptionDetail
|
||||
crystalMine: ConsumptionDetail
|
||||
deuteriumSynthesizer: ConsumptionDetail
|
||||
// 设施建筑
|
||||
roboticsFactory: ConsumptionDetail
|
||||
naniteFactory: ConsumptionDetail
|
||||
shipyard: ConsumptionDetail
|
||||
researchLab: ConsumptionDetail
|
||||
missileSilo: ConsumptionDetail
|
||||
terraformer: ConsumptionDetail
|
||||
darkMatterCollector: ConsumptionDetail
|
||||
// 月球建筑
|
||||
sensorPhalanx: ConsumptionDetail
|
||||
jumpGate: ConsumptionDetail
|
||||
total: number
|
||||
}
|
||||
|
||||
@@ -390,10 +475,25 @@ export const calculateProductionBreakdown = (
|
||||
|
||||
const crystalFinal = crystalBase * (1 + totalResourceBonus / 100) * productionEfficiency
|
||||
|
||||
// 重氢合成器产量
|
||||
const deuteriumBase = deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel)
|
||||
// 重氢合成器产量(受温度影响)
|
||||
// OGame 原版规则:温度越低,重氢产量越高
|
||||
const deuteriumTempBonus = planetLogic.calculateDeuteriumTemperatureBonus(planet.temperature?.max ?? 0)
|
||||
const deuteriumBase = deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * deuteriumTempBonus
|
||||
const deuteriumBonuses: ProductionBonus[] = []
|
||||
|
||||
// 温度加成显示
|
||||
if (planet.temperature) {
|
||||
const tempBonusPercent = Math.round((deuteriumTempBonus - 1) * 100)
|
||||
if (tempBonusPercent !== 0) {
|
||||
deuteriumBonuses.push({
|
||||
name: 'resources.temperatureBonus',
|
||||
percentage: tempBonusPercent,
|
||||
value: 0, // 已计入基础产量
|
||||
source: 'other'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
activeOfficerBonuses.forEach(officer => {
|
||||
if (officer.resourceBonus > 0) {
|
||||
const bonusValue = deuteriumBase * (officer.resourceBonus / 100)
|
||||
@@ -441,7 +541,11 @@ export const calculateProductionBreakdown = (
|
||||
|
||||
const solarPlantProduction = solarPlantLevel * 50 * Math.pow(1.1, solarPlantLevel)
|
||||
const fusionReactorProduction = fusionReactorLevel * 150 * Math.pow(1.15, fusionReactorLevel)
|
||||
const solarSatelliteProduction = solarSatelliteCount * 50
|
||||
|
||||
// 太阳能卫星产出:基于星球温度计算
|
||||
const maxTemp = planet.temperature?.max ?? 0
|
||||
const solarSatelliteOutputPerUnit = planetLogic.calculateSolarSatelliteOutput(maxTemp)
|
||||
const solarSatelliteProduction = solarSatelliteCount * solarSatelliteOutputPerUnit
|
||||
|
||||
const energyBase = solarPlantProduction + fusionReactorProduction + solarSatelliteProduction
|
||||
|
||||
@@ -541,17 +645,60 @@ export const calculateProductionBreakdown = (
|
||||
* 计算能量消耗详细breakdown
|
||||
*/
|
||||
export const calculateConsumptionBreakdown = (planet: Planet, resourceSpeed: number = 1): ConsumptionBreakdown => {
|
||||
// 资源建筑
|
||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
|
||||
|
||||
// 设施建筑
|
||||
const roboticsFactoryLevel = planet.buildings[BuildingType.RoboticsFactory] || 0
|
||||
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
|
||||
const shipyardLevel = planet.buildings[BuildingType.Shipyard] || 0
|
||||
const researchLabLevel = planet.buildings[BuildingType.ResearchLab] || 0
|
||||
const missileSiloLevel = planet.buildings[BuildingType.MissileSilo] || 0
|
||||
const terraformerLevel = planet.buildings[BuildingType.Terraformer] || 0
|
||||
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||
|
||||
// 月球建筑
|
||||
const sensorPhalanxLevel = planet.buildings[BuildingType.SensorPhalanx] || 0
|
||||
const jumpGateLevel = planet.buildings[BuildingType.JumpGate] || 0
|
||||
|
||||
// 资源建筑消耗
|
||||
const metalConsumption = metalMineLevel * 10 * Math.pow(1.1, metalMineLevel)
|
||||
const crystalConsumption = crystalMineLevel * 10 * Math.pow(1.1, crystalMineLevel)
|
||||
const deuteriumConsumption = deuteriumSynthesizerLevel * 15 * Math.pow(1.1, deuteriumSynthesizerLevel)
|
||||
|
||||
// 设施建筑消耗
|
||||
const roboticsConsumption = roboticsFactoryLevel * 5 * Math.pow(1.1, roboticsFactoryLevel)
|
||||
const naniteConsumption = naniteFactoryLevel * 20 * Math.pow(1.15, naniteFactoryLevel)
|
||||
const shipyardConsumption = shipyardLevel * 8 * Math.pow(1.1, shipyardLevel)
|
||||
const researchLabConsumption = researchLabLevel * 12 * Math.pow(1.1, researchLabLevel)
|
||||
const missileSiloConsumption = missileSiloLevel * 8 * Math.pow(1.1, missileSiloLevel)
|
||||
const terraformerConsumption = terraformerLevel * 25 * Math.pow(1.15, terraformerLevel)
|
||||
const darkMatterCollectorConsumption = darkMatterCollectorLevel * 10 * Math.pow(1.1, darkMatterCollectorLevel)
|
||||
|
||||
// 月球建筑消耗
|
||||
const sensorPhalanxConsumption = sensorPhalanxLevel * 15 * Math.pow(1.12, sensorPhalanxLevel)
|
||||
const jumpGateConsumption = jumpGateLevel * 50 * Math.pow(1.2, jumpGateLevel)
|
||||
|
||||
const speed = resourceSpeed
|
||||
|
||||
const total =
|
||||
metalConsumption +
|
||||
crystalConsumption +
|
||||
deuteriumConsumption +
|
||||
roboticsConsumption +
|
||||
naniteConsumption +
|
||||
shipyardConsumption +
|
||||
researchLabConsumption +
|
||||
missileSiloConsumption +
|
||||
terraformerConsumption +
|
||||
darkMatterCollectorConsumption +
|
||||
sensorPhalanxConsumption +
|
||||
jumpGateConsumption
|
||||
|
||||
return {
|
||||
// 资源建筑
|
||||
metalMine: {
|
||||
buildingLevel: metalMineLevel,
|
||||
buildingName: 'buildings.metalMine',
|
||||
@@ -567,6 +714,53 @@ export const calculateConsumptionBreakdown = (planet: Planet, resourceSpeed: num
|
||||
buildingName: 'buildings.deuteriumSynthesizer',
|
||||
consumption: deuteriumConsumption * speed
|
||||
},
|
||||
total: (metalConsumption + crystalConsumption + deuteriumConsumption) * speed
|
||||
// 设施建筑
|
||||
roboticsFactory: {
|
||||
buildingLevel: roboticsFactoryLevel,
|
||||
buildingName: 'buildings.roboticsFactory',
|
||||
consumption: roboticsConsumption * speed
|
||||
},
|
||||
naniteFactory: {
|
||||
buildingLevel: naniteFactoryLevel,
|
||||
buildingName: 'buildings.naniteFactory',
|
||||
consumption: naniteConsumption * speed
|
||||
},
|
||||
shipyard: {
|
||||
buildingLevel: shipyardLevel,
|
||||
buildingName: 'buildings.shipyard',
|
||||
consumption: shipyardConsumption * speed
|
||||
},
|
||||
researchLab: {
|
||||
buildingLevel: researchLabLevel,
|
||||
buildingName: 'buildings.researchLab',
|
||||
consumption: researchLabConsumption * speed
|
||||
},
|
||||
missileSilo: {
|
||||
buildingLevel: missileSiloLevel,
|
||||
buildingName: 'buildings.missileSilo',
|
||||
consumption: missileSiloConsumption * speed
|
||||
},
|
||||
terraformer: {
|
||||
buildingLevel: terraformerLevel,
|
||||
buildingName: 'buildings.terraformer',
|
||||
consumption: terraformerConsumption * speed
|
||||
},
|
||||
darkMatterCollector: {
|
||||
buildingLevel: darkMatterCollectorLevel,
|
||||
buildingName: 'buildings.darkMatterCollector',
|
||||
consumption: darkMatterCollectorConsumption * speed
|
||||
},
|
||||
// 月球建筑
|
||||
sensorPhalanx: {
|
||||
buildingLevel: sensorPhalanxLevel,
|
||||
buildingName: 'buildings.sensorPhalanx',
|
||||
consumption: sensorPhalanxConsumption * speed
|
||||
},
|
||||
jumpGate: {
|
||||
buildingLevel: jumpGateLevel,
|
||||
buildingName: 'buildings.jumpGate',
|
||||
consumption: jumpGateConsumption * speed
|
||||
},
|
||||
total: total * speed
|
||||
}
|
||||
}
|
||||
|
||||
135
src/logic/unlockLogic.ts
Normal file
135
src/logic/unlockLogic.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 解锁检查逻辑
|
||||
* 用于检测建筑/科技完成后新解锁的内容
|
||||
*/
|
||||
|
||||
import { BuildingType, TechnologyType, type Planet } from '@/types/game'
|
||||
import { BUILDINGS, TECHNOLOGIES } from '@/config/gameConfig'
|
||||
import * as buildingLogic from './buildingLogic'
|
||||
import * as researchLogic from './researchLogic'
|
||||
|
||||
export interface UnlockedItem {
|
||||
type: 'building' | 'technology'
|
||||
id: BuildingType | TechnologyType
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查哪些建筑在完成建筑/科技后被新解锁
|
||||
*/
|
||||
export const checkNewlyUnlockedBuildings = (
|
||||
planet: Planet,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
previousBuildings: Partial<Record<BuildingType, number>>,
|
||||
previousTechnologies: Partial<Record<TechnologyType, number>>
|
||||
): UnlockedItem[] => {
|
||||
const newlyUnlocked: UnlockedItem[] = []
|
||||
|
||||
// 遍历所有建筑类型
|
||||
for (const buildingType of Object.values(BuildingType)) {
|
||||
const config = BUILDINGS[buildingType]
|
||||
if (!config) continue
|
||||
|
||||
// 跳过已经建造过的建筑(等级 > 0)
|
||||
if ((planet.buildings[buildingType] || 0) > 0) continue
|
||||
|
||||
// 检查之前是否已解锁
|
||||
const wasUnlockedBefore = checkBuildingRequirementsWith(buildingType, previousBuildings, previousTechnologies)
|
||||
|
||||
// 检查现在是否解锁
|
||||
const isUnlockedNow = buildingLogic.checkBuildingRequirements(buildingType, planet, technologies)
|
||||
|
||||
// 如果之前未解锁,现在解锁了,则是新解锁
|
||||
if (!wasUnlockedBefore && isUnlockedNow) {
|
||||
newlyUnlocked.push({
|
||||
type: 'building',
|
||||
id: buildingType,
|
||||
name: config.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return newlyUnlocked
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查哪些科技在完成建筑/科技后被新解锁
|
||||
*/
|
||||
export const checkNewlyUnlockedTechnologies = (
|
||||
buildings: Partial<Record<BuildingType, number>>,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
previousBuildings: Partial<Record<BuildingType, number>>,
|
||||
previousTechnologies: Partial<Record<TechnologyType, number>>
|
||||
): UnlockedItem[] => {
|
||||
const newlyUnlocked: UnlockedItem[] = []
|
||||
|
||||
// 遍历所有科技类型
|
||||
for (const techType of Object.values(TechnologyType)) {
|
||||
const config = TECHNOLOGIES[techType]
|
||||
if (!config) continue
|
||||
|
||||
// 跳过已经研究过的科技(等级 > 0)
|
||||
if ((technologies[techType] || 0) > 0) continue
|
||||
|
||||
// 检查之前是否已解锁
|
||||
const wasUnlockedBefore = researchLogic.checkTechnologyRequirements(techType, previousBuildings, previousTechnologies)
|
||||
|
||||
// 检查现在是否解锁
|
||||
const isUnlockedNow = researchLogic.checkTechnologyRequirements(techType, buildings, technologies)
|
||||
|
||||
// 如果之前未解锁,现在解锁了,则是新解锁
|
||||
if (!wasUnlockedBefore && isUnlockedNow) {
|
||||
newlyUnlocked.push({
|
||||
type: 'technology',
|
||||
id: techType,
|
||||
name: config.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return newlyUnlocked
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的建筑和科技等级检查建筑需求
|
||||
* (用于与之前状态比较)
|
||||
*/
|
||||
const checkBuildingRequirementsWith = (
|
||||
buildingType: BuildingType,
|
||||
buildings: Partial<Record<BuildingType, number>>,
|
||||
technologies: Partial<Record<TechnologyType, number>>
|
||||
): boolean => {
|
||||
const config = BUILDINGS[buildingType]
|
||||
const requirements = (config as any).requirements
|
||||
if (!requirements) return true
|
||||
|
||||
for (const [key, level] of Object.entries(requirements)) {
|
||||
const requiredLevel = level as number
|
||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||
if ((buildings[key as BuildingType] || 0) < requiredLevel) {
|
||||
return false
|
||||
}
|
||||
} else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||
if ((technologies[key as TechnologyType] || 0) < requiredLevel) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查所有新解锁的内容(建筑 + 科技)
|
||||
*/
|
||||
export const checkAllNewlyUnlocked = (
|
||||
planet: Planet,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
previousBuildings: Partial<Record<BuildingType, number>>,
|
||||
previousTechnologies: Partial<Record<TechnologyType, number>>
|
||||
): UnlockedItem[] => {
|
||||
const unlockedBuildings = checkNewlyUnlockedBuildings(planet, technologies, previousBuildings, previousTechnologies)
|
||||
|
||||
const unlockedTechnologies = checkNewlyUnlockedTechnologies(planet.buildings, technologies, previousBuildings, previousTechnologies)
|
||||
|
||||
return [...unlockedBuildings, ...unlockedTechnologies]
|
||||
}
|
||||
518
src/logic/waitingQueueLogic.ts
Normal file
518
src/logic/waitingQueueLogic.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* 等待队列逻辑模块
|
||||
* 处理建筑、科技、舰船、防御的等待队列功能
|
||||
*/
|
||||
|
||||
import type { Planet, Player, WaitingQueueItem, Resources, Officer, BuildQueueItem } from '@/types/game'
|
||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||
import * as buildingLogic from './buildingLogic'
|
||||
import * as researchLogic from './researchLogic'
|
||||
import * as shipLogic from './shipLogic'
|
||||
import * as resourceLogic from './resourceLogic'
|
||||
import * as publicLogic from './publicLogic'
|
||||
import * as officerLogic from './officerLogic'
|
||||
import * as buildingValidation from './buildingValidation'
|
||||
import * as researchValidation from './researchValidation'
|
||||
import * as shipValidation from './shipValidation'
|
||||
|
||||
/**
|
||||
* 获取等待队列最大容量
|
||||
* 与正式队列相同(建筑: 3 + 纳米工厂等级,科技: 3 + 计算机技术等级,最多10)
|
||||
*/
|
||||
export const getMaxBuildWaitingQueue = (planet: Planet, additionalBuildQueue: number = 0): number => {
|
||||
return publicLogic.getMaxBuildQueue(planet, additionalBuildQueue)
|
||||
}
|
||||
|
||||
export const getMaxResearchWaitingQueue = (technologies: Partial<Record<TechnologyType, number>>): number => {
|
||||
return publicLogic.getMaxResearchQueue(technologies)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建建筑等待队列项
|
||||
*/
|
||||
export const createBuildingWaitingItem = (buildingType: BuildingType, targetLevel: number, planetId: string): WaitingQueueItem => {
|
||||
return {
|
||||
id: `waiting_building_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'building',
|
||||
itemType: buildingType,
|
||||
targetLevel,
|
||||
priority: Date.now(),
|
||||
addedTime: Date.now(),
|
||||
planetId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拆除等待队列项
|
||||
*/
|
||||
export const createDemolishWaitingItem = (buildingType: BuildingType, targetLevel: number, planetId: string): WaitingQueueItem => {
|
||||
return {
|
||||
id: `waiting_demolish_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'demolish',
|
||||
itemType: buildingType,
|
||||
targetLevel,
|
||||
priority: Date.now(),
|
||||
addedTime: Date.now(),
|
||||
planetId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建科技等待队列项
|
||||
*/
|
||||
export const createResearchWaitingItem = (techType: TechnologyType, targetLevel: number): WaitingQueueItem => {
|
||||
return {
|
||||
id: `waiting_tech_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'technology',
|
||||
itemType: techType,
|
||||
targetLevel,
|
||||
priority: Date.now(),
|
||||
addedTime: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建舰船等待队列项
|
||||
*/
|
||||
export const createShipWaitingItem = (shipType: ShipType, quantity: number, planetId: string): WaitingQueueItem => {
|
||||
return {
|
||||
id: `waiting_ship_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'ship',
|
||||
itemType: shipType,
|
||||
quantity,
|
||||
priority: Date.now(),
|
||||
addedTime: Date.now(),
|
||||
planetId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建防御等待队列项
|
||||
*/
|
||||
export const createDefenseWaitingItem = (defenseType: DefenseType, quantity: number, planetId: string): WaitingQueueItem => {
|
||||
return {
|
||||
id: `waiting_defense_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: 'defense',
|
||||
itemType: defenseType,
|
||||
quantity,
|
||||
priority: Date.now(),
|
||||
addedTime: Date.now(),
|
||||
planetId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算等待队列项的预估成本
|
||||
*/
|
||||
export const calculateWaitingItemCost = (item: WaitingQueueItem): Resources => {
|
||||
switch (item.type) {
|
||||
case 'building':
|
||||
return buildingLogic.calculateBuildingCost(item.itemType as BuildingType, item.targetLevel || 1)
|
||||
case 'demolish':
|
||||
// 拆除不需要资源
|
||||
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
case 'technology':
|
||||
return researchLogic.calculateTechnologyCost(item.itemType as TechnologyType, item.targetLevel || 1)
|
||||
case 'ship':
|
||||
return shipLogic.calculateShipCost(item.itemType as ShipType, item.quantity || 1)
|
||||
case 'defense':
|
||||
return shipLogic.calculateDefenseCost(item.itemType as DefenseType, item.quantity || 1)
|
||||
default:
|
||||
return { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源是否足够执行等待队列项
|
||||
*/
|
||||
export const canExecuteWaitingItem = (item: WaitingQueueItem, resources: Resources): boolean => {
|
||||
const cost = calculateWaitingItemCost(item)
|
||||
return resourceLogic.checkResourcesAvailable(resources, cost)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以添加到建筑等待队列
|
||||
*/
|
||||
export const canAddToBuildWaitingQueue = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { canAdd: boolean; reason?: string } => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
const maxQueue = getMaxBuildWaitingQueue(planet, bonuses.additionalBuildQueue)
|
||||
const waitingQueue = planet.waitingBuildQueue || []
|
||||
|
||||
if (waitingQueue.length >= maxQueue) {
|
||||
return { canAdd: false, reason: 'errors.waitingQueueFull' }
|
||||
}
|
||||
|
||||
// 建筑允许多次排队(比如金属矿升级到2、3、4、5级)
|
||||
// 拆除类型:不允许重复排队
|
||||
if (item.type === 'demolish') {
|
||||
const buildingType = item.itemType as BuildingType
|
||||
const existsInWaiting = waitingQueue.some(q => q.type === 'demolish' && q.itemType === buildingType)
|
||||
const existsInQueue = planet.buildQueue.some(q => q.type === 'demolish' && q.itemType === buildingType)
|
||||
if (existsInWaiting || existsInQueue) {
|
||||
return { canAdd: false, reason: 'errors.buildingAlreadyInQueue' }
|
||||
}
|
||||
}
|
||||
|
||||
return { canAdd: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以添加到研究等待队列
|
||||
*/
|
||||
export const canAddToResearchWaitingQueue = (player: Player, _item: WaitingQueueItem): { canAdd: boolean; reason?: string } => {
|
||||
const maxQueue = getMaxResearchWaitingQueue(player.technologies)
|
||||
const waitingQueue = player.waitingResearchQueue || []
|
||||
|
||||
if (waitingQueue.length >= maxQueue) {
|
||||
return { canAdd: false, reason: 'errors.waitingQueueFull' }
|
||||
}
|
||||
|
||||
// 科技允许多次排队(比如能源技术升级到2、3、4、5级)
|
||||
return { canAdd: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到建筑等待队列
|
||||
*/
|
||||
export const addToBuildWaitingQueue = (planet: Planet, item: WaitingQueueItem): void => {
|
||||
// 确保数组存在(向后兼容)
|
||||
if (!planet.waitingBuildQueue) {
|
||||
planet.waitingBuildQueue = []
|
||||
}
|
||||
planet.waitingBuildQueue.push(item)
|
||||
// 按优先级排序(数字越小越靠前)
|
||||
planet.waitingBuildQueue.sort((a, b) => a.priority - b.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到研究等待队列
|
||||
*/
|
||||
export const addToResearchWaitingQueue = (player: Player, item: WaitingQueueItem): void => {
|
||||
// 确保数组存在(向后兼容)
|
||||
if (!player.waitingResearchQueue) {
|
||||
player.waitingResearchQueue = []
|
||||
}
|
||||
player.waitingResearchQueue.push(item)
|
||||
// 按优先级排序
|
||||
player.waitingResearchQueue.sort((a, b) => a.priority - b.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从建筑等待队列移除
|
||||
*/
|
||||
export const removeFromBuildWaitingQueue = (planet: Planet, itemId: string): WaitingQueueItem | null => {
|
||||
if (!planet.waitingBuildQueue) return null
|
||||
const index = planet.waitingBuildQueue.findIndex(q => q.id === itemId)
|
||||
if (index === -1) return null
|
||||
const [removed] = planet.waitingBuildQueue.splice(index, 1)
|
||||
return removed ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从研究等待队列移除
|
||||
*/
|
||||
export const removeFromResearchWaitingQueue = (player: Player, itemId: string): WaitingQueueItem | null => {
|
||||
if (!player.waitingResearchQueue) return null
|
||||
const index = player.waitingResearchQueue.findIndex(q => q.id === itemId)
|
||||
if (index === -1) return null
|
||||
const [removed] = player.waitingResearchQueue.splice(index, 1)
|
||||
return removed ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个星球的建筑等待队列
|
||||
* 当正式队列为空且资源足够时,自动将等待队列项移至正式队列
|
||||
*/
|
||||
export const processPlanetWaitingQueue = (
|
||||
planet: Planet,
|
||||
player: Player,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { executed: WaitingQueueItem[]; messages: string[] } => {
|
||||
const executed: WaitingQueueItem[] = []
|
||||
const messages: string[] = []
|
||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||
|
||||
// 检查正式队列是否为空
|
||||
const maxBuildQueue = publicLogic.getMaxBuildQueue(planet, bonuses.additionalBuildQueue)
|
||||
const buildingQueueCount = planet.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
|
||||
// 队列已满,不处理
|
||||
if (buildingQueueCount >= maxBuildQueue) {
|
||||
return { executed, messages }
|
||||
}
|
||||
|
||||
// 遍历等待队列(已按优先级排序)
|
||||
const waitingQueue = planet.waitingBuildQueue || []
|
||||
const itemsToProcess = [...waitingQueue]
|
||||
for (const waitingItem of itemsToProcess) {
|
||||
// 检查正式队列是否还有空位
|
||||
const currentBuildQueueCount = planet.buildQueue.filter(item => item.type === 'building' || item.type === 'demolish').length
|
||||
if (currentBuildQueueCount >= maxBuildQueue) break
|
||||
|
||||
// 根据类型执行不同处理
|
||||
if (waitingItem.type === 'building') {
|
||||
const result = tryExecuteBuildingWaitingItem(planet, waitingItem, player.technologies, officers)
|
||||
if (result.success && result.queueItem) {
|
||||
planet.buildQueue.push(result.queueItem)
|
||||
removeFromBuildWaitingQueue(planet, waitingItem.id)
|
||||
executed.push(waitingItem)
|
||||
messages.push(result.message || '')
|
||||
}
|
||||
} else if (waitingItem.type === 'demolish') {
|
||||
const result = tryExecuteDemolishWaitingItem(planet, waitingItem, officers)
|
||||
if (result.success && result.queueItem) {
|
||||
planet.buildQueue.push(result.queueItem)
|
||||
removeFromBuildWaitingQueue(planet, waitingItem.id)
|
||||
executed.push(waitingItem)
|
||||
messages.push(result.message || '')
|
||||
}
|
||||
} else if (waitingItem.type === 'ship') {
|
||||
const result = tryExecuteShipWaitingItem(planet, waitingItem, player.technologies, officers)
|
||||
if (result.success && result.queueItem) {
|
||||
planet.buildQueue.push(result.queueItem)
|
||||
removeFromBuildWaitingQueue(planet, waitingItem.id)
|
||||
executed.push(waitingItem)
|
||||
messages.push(result.message || '')
|
||||
}
|
||||
} else if (waitingItem.type === 'defense') {
|
||||
const result = tryExecuteDefenseWaitingItem(planet, waitingItem, player.technologies, officers)
|
||||
if (result.success && result.queueItem) {
|
||||
planet.buildQueue.push(result.queueItem)
|
||||
removeFromBuildWaitingQueue(planet, waitingItem.id)
|
||||
executed.push(waitingItem)
|
||||
messages.push(result.message || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { executed, messages }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理研究等待队列
|
||||
*/
|
||||
export const processResearchWaitingQueue = (
|
||||
player: Player,
|
||||
currentPlanet: Planet,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { executed: WaitingQueueItem[]; messages: string[] } => {
|
||||
const executed: WaitingQueueItem[] = []
|
||||
const messages: string[] = []
|
||||
|
||||
// 检查正式队列是否为空
|
||||
const maxResearchQueue = publicLogic.getMaxResearchQueue(player.technologies)
|
||||
if (player.researchQueue.length >= maxResearchQueue) {
|
||||
return { executed, messages }
|
||||
}
|
||||
|
||||
// 遍历等待队列
|
||||
const waitingResearchQueue = player.waitingResearchQueue || []
|
||||
const itemsToProcess = [...waitingResearchQueue]
|
||||
for (const waitingItem of itemsToProcess) {
|
||||
if (player.researchQueue.length >= maxResearchQueue) break
|
||||
|
||||
if (waitingItem.type === 'technology') {
|
||||
const result = tryExecuteResearchWaitingItem(currentPlanet, waitingItem, player, officers)
|
||||
if (result.success && result.queueItem) {
|
||||
player.researchQueue.push(result.queueItem)
|
||||
removeFromResearchWaitingQueue(player, waitingItem.id)
|
||||
executed.push(waitingItem)
|
||||
messages.push(result.message || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { executed, messages }
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试执行建筑等待项
|
||||
*/
|
||||
const tryExecuteBuildingWaitingItem = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { success: boolean; queueItem?: BuildQueueItem; message?: string } => {
|
||||
const buildingType = item.itemType as BuildingType
|
||||
const currentLevel = planet.buildings[buildingType] || 0
|
||||
const targetLevel = item.targetLevel || currentLevel + 1
|
||||
|
||||
// 检查目标等级是否仍然正确(可能在等待期间已经升级了)
|
||||
if (currentLevel >= targetLevel) {
|
||||
return { success: false, message: 'errors.levelAlreadyReached' }
|
||||
}
|
||||
|
||||
// 验证升级条件(跳过队列已满检查,因为我们已经检查过了)
|
||||
const validation = buildingValidation.validateBuildingUpgrade(planet, buildingType, technologies, officers)
|
||||
if (!validation.valid && validation.reason !== 'errors.buildQueueFull') {
|
||||
return { success: false, message: validation.reason }
|
||||
}
|
||||
|
||||
// 重新检查资源(因为validateBuildingUpgrade可能因资源不足失败)
|
||||
const cost = buildingLogic.calculateBuildingCost(buildingType, targetLevel)
|
||||
if (!resourceLogic.checkResourcesAvailable(planet.resources, cost)) {
|
||||
return { success: false, message: 'errors.insufficientResources' }
|
||||
}
|
||||
|
||||
// 执行升级
|
||||
const queueItem = buildingValidation.executeBuildingUpgrade(planet, buildingType, officers)
|
||||
return { success: true, queueItem, message: 'queue.movedToQueue' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试执行拆除等待项
|
||||
*/
|
||||
const tryExecuteDemolishWaitingItem = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { success: boolean; queueItem?: BuildQueueItem; message?: string } => {
|
||||
const buildingType = item.itemType as BuildingType
|
||||
const currentLevel = planet.buildings[buildingType] || 0
|
||||
const targetLevel = item.targetLevel || currentLevel - 1
|
||||
|
||||
// 检查等级是否仍然可拆除
|
||||
if (currentLevel <= 0 || currentLevel <= targetLevel) {
|
||||
return { success: false, message: 'errors.buildingLevelZero' }
|
||||
}
|
||||
|
||||
// 验证拆除条件
|
||||
const validation = buildingValidation.validateBuildingDemolish(planet, buildingType, officers)
|
||||
if (!validation.valid && validation.reason !== 'errors.buildQueueFull') {
|
||||
return { success: false, message: validation.reason }
|
||||
}
|
||||
|
||||
// 执行拆除
|
||||
const queueItem = buildingValidation.executeBuildingDemolish(planet, buildingType, officers)
|
||||
return { success: true, queueItem, message: 'queue.movedToQueue' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试执行舰船等待项
|
||||
*/
|
||||
const tryExecuteShipWaitingItem = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { success: boolean; queueItem?: BuildQueueItem; message?: string } => {
|
||||
const shipType = item.itemType as ShipType
|
||||
const quantity = item.quantity || 1
|
||||
|
||||
// 验证建造条件
|
||||
const validation = shipValidation.validateShipBuild(planet, shipType, quantity, technologies)
|
||||
if (!validation.valid) {
|
||||
return { success: false, message: validation.reason }
|
||||
}
|
||||
|
||||
// 执行建造
|
||||
const queueItem = shipValidation.executeShipBuild(planet, shipType, quantity, officers)
|
||||
return { success: true, queueItem, message: 'queue.movedToQueue' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试执行防御等待项
|
||||
*/
|
||||
const tryExecuteDefenseWaitingItem = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
technologies: Partial<Record<TechnologyType, number>>,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { success: boolean; queueItem?: BuildQueueItem; message?: string } => {
|
||||
const defenseType = item.itemType as DefenseType
|
||||
const quantity = item.quantity || 1
|
||||
|
||||
// 验证建造条件
|
||||
const validation = shipValidation.validateDefenseBuild(planet, defenseType, quantity, technologies)
|
||||
if (!validation.valid) {
|
||||
return { success: false, message: validation.reason }
|
||||
}
|
||||
|
||||
// 执行建造
|
||||
const queueItem = shipValidation.executeDefenseBuild(planet, defenseType, quantity, officers)
|
||||
return { success: true, queueItem, message: 'queue.movedToQueue' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试执行研究等待项
|
||||
*/
|
||||
const tryExecuteResearchWaitingItem = (
|
||||
planet: Planet,
|
||||
item: WaitingQueueItem,
|
||||
player: Player,
|
||||
officers: Record<OfficerType, Officer>
|
||||
): { success: boolean; queueItem?: BuildQueueItem; message?: string } => {
|
||||
const techType = item.itemType as TechnologyType
|
||||
const currentLevel = player.technologies[techType] || 0
|
||||
const targetLevel = item.targetLevel || currentLevel + 1
|
||||
|
||||
// 检查目标等级是否仍然正确
|
||||
if (currentLevel >= targetLevel) {
|
||||
return { success: false, message: 'errors.levelAlreadyReached' }
|
||||
}
|
||||
|
||||
// 验证研究条件
|
||||
const validation = researchValidation.validateTechnologyResearch(planet, techType, player.technologies, player.researchQueue)
|
||||
if (!validation.valid && validation.reason !== 'errors.researchQueueFull') {
|
||||
return { success: false, message: validation.reason }
|
||||
}
|
||||
|
||||
// 重新检查资源
|
||||
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
|
||||
if (!resourceLogic.checkResourcesAvailable(planet.resources, cost)) {
|
||||
return { success: false, message: 'errors.insufficientResources' }
|
||||
}
|
||||
|
||||
// 执行研究
|
||||
const result = researchValidation.executeTechnologyResearch(planet, techType, currentLevel, officers, player.technologies)
|
||||
return { success: true, queueItem: result.queueItem, message: 'queue.movedToQueue' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有等待队列(在 gameLoop 中调用)
|
||||
*/
|
||||
export const processAllWaitingQueues = (player: Player, _now: number): { executed: WaitingQueueItem[]; messages: string[] } => {
|
||||
const allExecuted: WaitingQueueItem[] = []
|
||||
const allMessages: string[] = []
|
||||
|
||||
// 处理所有星球的建筑等待队列
|
||||
for (const planet of player.planets) {
|
||||
const result = processPlanetWaitingQueue(planet, player, player.officers)
|
||||
allExecuted.push(...result.executed)
|
||||
allMessages.push(...result.messages)
|
||||
}
|
||||
|
||||
// 处理研究等待队列(使用第一个有研究实验室的星球)
|
||||
const labPlanet = player.planets.find(p => (p.buildings[BuildingType.ResearchLab] || 0) > 0)
|
||||
if (labPlanet) {
|
||||
const result = processResearchWaitingQueue(player, labPlanet, player.officers)
|
||||
allExecuted.push(...result.executed)
|
||||
allMessages.push(...result.messages)
|
||||
}
|
||||
|
||||
return { executed: allExecuted, messages: allMessages }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等待队列项的显示名称
|
||||
*/
|
||||
export const getWaitingItemName = (item: WaitingQueueItem): string => {
|
||||
switch (item.type) {
|
||||
case 'building':
|
||||
case 'demolish':
|
||||
return `buildings.${item.itemType}`
|
||||
case 'technology':
|
||||
return `technologies.${item.itemType}`
|
||||
case 'ship':
|
||||
return `ships.${item.itemType}`
|
||||
case 'defense':
|
||||
return `defenses.${item.itemType}`
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const router = createRouter({
|
||||
{ path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') },
|
||||
{ path: '/diplomacy', name: 'diplomacy', component: () => import('@/views/DiplomacyView.vue') },
|
||||
{ path: '/achievements', name: 'achievements', component: () => import('@/views/AchievementsView.vue') },
|
||||
{ path: '/campaign', name: 'campaign', component: () => import('@/views/CampaignView.vue') },
|
||||
{ path: '/ranking', name: 'ranking', component: () => import('@/views/RankingView.vue') },
|
||||
{ path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
|
||||
{ path: '/gm', name: 'gm', component: () => import('@/views/GMView.vue') },
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue') }
|
||||
|
||||
@@ -32,6 +32,7 @@ export const useGameStore = defineStore('game', {
|
||||
technologies: {} as Record<TechnologyType, number>,
|
||||
officers: {} as Record<OfficerType, Officer>,
|
||||
researchQueue: [] as BuildQueueItem[],
|
||||
waitingResearchQueue: [],
|
||||
fleetMissions: [] as FleetMission[],
|
||||
missileAttacks: [] as MissileAttack[],
|
||||
battleReports: [] as BattleResult[],
|
||||
@@ -57,7 +58,8 @@ export const useGameStore = defineStore('game', {
|
||||
suppressInFocus: false,
|
||||
types: {
|
||||
construction: true,
|
||||
research: true
|
||||
research: true,
|
||||
unlock: true
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -194,3 +194,27 @@ aside nav a:hover button {
|
||||
.resource-pulse {
|
||||
animation: pulse-resource 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Popover 和 Tooltip 样式修复 - 使用硬编码颜色 */
|
||||
.popover-content-custom {
|
||||
background-color: oklch(0.15 0.02 264.695) !important;
|
||||
color: oklch(0.984 0.003 247.858) !important;
|
||||
border-color: oklch(0.25 0.025 260.031) !important;
|
||||
}
|
||||
|
||||
/* 亮色模式 */
|
||||
:root:not(.dark) .popover-content-custom {
|
||||
background-color: oklch(0.97 0.006 85) !important;
|
||||
color: oklch(0.3 0.02 85) !important;
|
||||
border-color: oklch(0.86 0.012 85) !important;
|
||||
}
|
||||
|
||||
.tooltip-content-custom {
|
||||
background-color: oklch(0.984 0.003 247.858) !important;
|
||||
color: oklch(0.1 0.015 264.695) !important;
|
||||
}
|
||||
|
||||
:root:not(.dark) .tooltip-content-custom {
|
||||
background-color: oklch(0.3 0.02 85) !important;
|
||||
color: oklch(0.95 0.008 85) !important;
|
||||
}
|
||||
@@ -14,6 +14,16 @@ export interface Resources {
|
||||
energy: number // 电量(实时计算,不存储)
|
||||
}
|
||||
|
||||
// 星球矿脉储量
|
||||
export interface OreDeposits {
|
||||
metal: number // 金属矿脉剩余储量
|
||||
crystal: number // 晶体矿脉剩余储量
|
||||
deuterium: number // 重氢储量
|
||||
initialMetal: number // 初始金属储量(用于计算百分比)
|
||||
initialCrystal: number // 初始晶体储量
|
||||
initialDeuterium: number // 初始重氢储量
|
||||
}
|
||||
|
||||
// 建筑类型
|
||||
export const BuildingType = {
|
||||
MetalMine: 'metalMine',
|
||||
@@ -213,6 +223,35 @@ export const MissionType = {
|
||||
|
||||
export type MissionType = (typeof MissionType)[keyof typeof MissionType]
|
||||
|
||||
// 探险区域类型
|
||||
export const ExpeditionZone = {
|
||||
NearSpace: 'nearSpace', // 近空区域 - 低风险低收益
|
||||
DeepSpace: 'deepSpace', // 深空区域 - 中等风险收益
|
||||
UnchartedSpace: 'unchartedSpace', // 未知空间 - 高风险高收益
|
||||
DangerousNebula: 'dangerousNebula' // 危险星云 - 极高风险极高收益
|
||||
} as const
|
||||
|
||||
export type ExpeditionZone = (typeof ExpeditionZone)[keyof typeof ExpeditionZone]
|
||||
|
||||
// 探险区域配置
|
||||
export interface ExpeditionZoneConfig {
|
||||
id: ExpeditionZone
|
||||
requiredTechLevel: number // 所需天体物理学等级
|
||||
flightTimeMultiplier: number // 飞行时间倍率
|
||||
resourceMultiplier: number // 资源奖励倍率
|
||||
darkMatterMultiplier: number // 暗物质奖励倍率
|
||||
fleetFindMultiplier: number // 舰船发现倍率
|
||||
dangerMultiplier: number // 危险程度倍率
|
||||
probabilities: {
|
||||
resources: number // 发现资源概率
|
||||
darkMatter: number // 发现暗物质概率
|
||||
fleet: number // 发现舰船概率
|
||||
pirates: number // 遭遇海盗概率
|
||||
aliens: number // 遭遇外星人概率
|
||||
nothing: number // 什么都没发现概率
|
||||
}
|
||||
}
|
||||
|
||||
// 外交关系状态
|
||||
export const RelationStatus = {
|
||||
Hostile: 'hostile', // 敌对
|
||||
@@ -298,6 +337,8 @@ export interface FleetMission {
|
||||
// 外交系统字段
|
||||
isGift?: boolean // 是否为赠送资源任务
|
||||
giftTargetNpcId?: string // 赠送目标NPC ID
|
||||
// 探险系统字段
|
||||
expeditionZone?: ExpeditionZone // 探险区域类型
|
||||
}
|
||||
|
||||
// 导弹攻击任务(不使用舰队系统)
|
||||
@@ -452,6 +493,8 @@ export interface MissionReport {
|
||||
foundFleet?: Partial<Fleet>
|
||||
// 探险任务:损失的舰船
|
||||
fleetLost?: Partial<Fleet>
|
||||
// 探险任务:探险区域
|
||||
expeditionZone?: ExpeditionZone
|
||||
// 侦查任务:报告ID
|
||||
spyReportId?: string
|
||||
}
|
||||
@@ -503,6 +546,18 @@ export interface BuildQueueItem {
|
||||
endTime: number
|
||||
}
|
||||
|
||||
// 等待队列项(尚未开始执行,不需要 startTime/endTime)
|
||||
export interface WaitingQueueItem {
|
||||
id: string
|
||||
type: 'building' | 'technology' | 'ship' | 'defense' | 'demolish'
|
||||
itemType: BuildingType | TechnologyType | ShipType | DefenseType
|
||||
targetLevel?: number // 用于建筑和科技
|
||||
quantity?: number // 用于舰船和防御
|
||||
priority: number // 排序优先级(数字越小优先级越高)
|
||||
addedTime: number // 添加到等待队列的时间戳
|
||||
planetId?: string // 建造队列需要标识目标星球
|
||||
}
|
||||
|
||||
// 星球
|
||||
export interface Planet {
|
||||
id: string
|
||||
@@ -514,6 +569,7 @@ export interface Planet {
|
||||
fleet: Fleet
|
||||
defense: Record<DefenseType, number>
|
||||
buildQueue: BuildQueueItem[]
|
||||
waitingBuildQueue: WaitingQueueItem[] // 等待队列(建筑、舰船、防御)
|
||||
lastUpdate: number
|
||||
maxSpace: number // 最大空间
|
||||
maxFleetStorage: number // 舰队仓储上限
|
||||
@@ -521,6 +577,8 @@ export interface Planet {
|
||||
parentPlanetId?: string // 如果是月球,指向母星的ID
|
||||
diameter?: number // 月球直径(km),用于销毁概率计算
|
||||
jumpGateLastUsed?: number // 跳跃门上次使用时间戳(ms),用于冷却计算
|
||||
oreDeposits?: OreDeposits // 矿脉储量(可选,用于向后兼容)
|
||||
temperature?: { min: number; max: number } // 星球温度范围(摄氏度),影响太阳能卫星和重氢产量
|
||||
}
|
||||
|
||||
// 月球特殊配置
|
||||
@@ -581,6 +639,7 @@ export interface Player {
|
||||
technologies: Record<TechnologyType, number>
|
||||
officers: Record<OfficerType, Officer>
|
||||
researchQueue: BuildQueueItem[]
|
||||
waitingResearchQueue: WaitingQueueItem[] // 研究等待队列
|
||||
fleetMissions: FleetMission[]
|
||||
missileAttacks: MissileAttack[] // 导弹攻击任务
|
||||
battleReports: BattleResult[]
|
||||
@@ -611,6 +670,9 @@ export interface Player {
|
||||
// 成就系统
|
||||
achievementStats?: AchievementStats // 成就统计数据
|
||||
achievements?: Record<string, AchievementProgress> // 成就进度
|
||||
// 战役系统
|
||||
campaignProgress?: PlayerCampaignProgress // 战役进度
|
||||
questNotifications?: QuestNotification[] // 任务通知
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
@@ -642,6 +704,17 @@ export interface Universe {
|
||||
npcs: NPC[]
|
||||
}
|
||||
|
||||
// NPC AI 类型
|
||||
export const NPCAIType = {
|
||||
Aggressive: 'aggressive', // 侵略型 - 积极侦查和攻击玩家
|
||||
Defensive: 'defensive', // 防守型 - 只在被攻击时反击,优先发展防御
|
||||
Trader: 'trader', // 商人型 - 主动交易,几乎不攻击,高好感度时赠送资源
|
||||
Expansionist: 'expansionist', // 扩张型 - 优先发展和殖民,较少攻击
|
||||
Balanced: 'balanced' // 平衡型 - 根据情况动态调整策略
|
||||
} as const
|
||||
|
||||
export type NPCAIType = (typeof NPCAIType)[keyof typeof NPCAIType]
|
||||
|
||||
// NPC玩家
|
||||
export interface NPC {
|
||||
id: string
|
||||
@@ -650,6 +723,7 @@ export interface NPC {
|
||||
planets: Planet[]
|
||||
technologies: Record<TechnologyType, number>
|
||||
difficulty: 'easy' | 'medium' | 'hard' // 保留兼容,不再使用
|
||||
aiType?: NPCAIType // AI 行为类型
|
||||
// 距离难度系统
|
||||
difficultyLevel?: number // 基于距离的难度等级(1-无限)
|
||||
distanceToHomeworld?: number // 到玩家母星的距离
|
||||
@@ -822,3 +896,190 @@ export interface AchievementProgress {
|
||||
unlockedAt?: number
|
||||
tierUnlocks: Record<AchievementTier, number | null>
|
||||
}
|
||||
|
||||
// ==================== 排行榜系统类型 ====================
|
||||
|
||||
// 排行榜类别枚举
|
||||
export const RankingCategory = {
|
||||
Total: 'total', // 总积分
|
||||
Building: 'building', // 建筑积分
|
||||
Research: 'research', // 研究积分
|
||||
Fleet: 'fleet', // 舰队积分
|
||||
Defense: 'defense' // 防御积分
|
||||
} as const
|
||||
export type RankingCategory = (typeof RankingCategory)[keyof typeof RankingCategory]
|
||||
|
||||
// 排行榜条目接口
|
||||
export interface RankingEntry {
|
||||
id: string // 玩家或NPC ID
|
||||
name: string // 名称
|
||||
isPlayer: boolean // 是否为玩家(否则为NPC)
|
||||
scores: {
|
||||
total: number // 总积分
|
||||
building: number // 建筑积分
|
||||
research: number // 研究积分
|
||||
fleet: number // 舰队积分
|
||||
defense: number // 防御积分
|
||||
}
|
||||
planetCount: number // 星球数量
|
||||
}
|
||||
|
||||
// ==================== 战役系统 ====================
|
||||
|
||||
// 任务状态
|
||||
export const QuestStatus = {
|
||||
Locked: 'locked', // 未解锁
|
||||
Available: 'available', // 可接取
|
||||
Active: 'active', // 进行中
|
||||
Completed: 'completed', // 已完成
|
||||
Failed: 'failed' // 失败
|
||||
} as const
|
||||
export type QuestStatus = (typeof QuestStatus)[keyof typeof QuestStatus]
|
||||
|
||||
// 任务目标类型
|
||||
export const ObjectiveType = {
|
||||
// 基础目标
|
||||
BuildBuilding: 'buildBuilding', // 建造建筑到某等级
|
||||
ResearchTech: 'researchTech', // 研究科技
|
||||
ProduceShips: 'produceShips', // 生产舰船
|
||||
AccumulateResources: 'accumulateResources', // 积累资源
|
||||
|
||||
// 战斗目标
|
||||
DefeatNPC: 'defeatNPC', // 击败特定NPC
|
||||
WinBattles: 'winBattles', // 赢得N场战斗
|
||||
RecycleDebris: 'recycleDebris', // 回收残骸
|
||||
DestroyPlanet: 'destroyPlanet', // 摧毁星球
|
||||
|
||||
// 外交目标
|
||||
ReachRelation: 'reachRelation', // 达到某关系等级
|
||||
SendGift: 'sendGift', // 送礼
|
||||
FormAlliance: 'formAlliance', // 结盟
|
||||
|
||||
// 探索目标
|
||||
Colonize: 'colonize', // 殖民新星球
|
||||
Expedition: 'expedition', // 完成探险任务
|
||||
DiscoverLocation: 'discoverLocation', // 发现特定位置
|
||||
SpyTarget: 'spyTarget' // 侦查特定目标
|
||||
} as const
|
||||
export type ObjectiveType = (typeof ObjectiveType)[keyof typeof ObjectiveType]
|
||||
|
||||
// 任务目标
|
||||
export interface QuestObjective {
|
||||
id: string
|
||||
type: ObjectiveType
|
||||
descriptionKey: string // 翻译键
|
||||
target: string | number // 目标值或ID (建筑类型/科技类型/舰船类型/NPC ID等)
|
||||
targetSecondary?: string | number // 次要目标 (如等级要求)
|
||||
required: number // 需要数量
|
||||
}
|
||||
|
||||
// 任务奖励
|
||||
export interface QuestReward {
|
||||
resources?: Partial<Resources>
|
||||
darkMatter?: number
|
||||
points?: number
|
||||
ships?: Partial<Fleet>
|
||||
unlockBuilding?: BuildingType
|
||||
unlockTech?: TechnologyType
|
||||
specialItem?: string // 特殊物品ID
|
||||
}
|
||||
|
||||
// 剧情对话
|
||||
export interface StoryDialogue {
|
||||
id: string
|
||||
speaker: 'narrator' | 'player' | 'npc' | 'mysterious'
|
||||
speakerNameKey?: string // 翻译键
|
||||
portrait?: string // 头像标识
|
||||
textKey: string // 翻译键
|
||||
choices?: DialogueChoice[]
|
||||
}
|
||||
|
||||
// 对话选项
|
||||
export interface DialogueChoice {
|
||||
textKey: string // 翻译键
|
||||
nextDialogueId?: string
|
||||
effect?: 'reputation_up' | 'reputation_down' | 'unlock_branch'
|
||||
}
|
||||
|
||||
// 战役任务配置
|
||||
export interface CampaignQuestConfig {
|
||||
id: string
|
||||
chapter: number // 章节号
|
||||
order: number // 章节内顺序
|
||||
titleKey: string // 翻译键
|
||||
descriptionKey: string // 翻译键
|
||||
|
||||
// 剧情
|
||||
prologueDialogues?: StoryDialogue[] // 任务开始对话
|
||||
epilogueDialogues?: StoryDialogue[] // 任务完成对话
|
||||
|
||||
// 目标
|
||||
objectives: QuestObjective[]
|
||||
|
||||
// 奖励
|
||||
rewards: QuestReward
|
||||
|
||||
// 解锁条件
|
||||
requiredQuestIds?: string[] // 需要先完成的任务
|
||||
|
||||
// 任务位置(用于地图显示)
|
||||
mapPosition: { x: number; y: number }
|
||||
|
||||
// 特殊标记
|
||||
isBoss?: boolean // Boss战
|
||||
isBranch?: boolean // 分支任务
|
||||
branchGroup?: string // 分支组ID(同组只能完成一个)
|
||||
}
|
||||
|
||||
// 战役章节配置
|
||||
export interface CampaignChapterConfig {
|
||||
id: string
|
||||
number: number
|
||||
titleKey: string // 翻译键
|
||||
descriptionKey: string // 翻译键
|
||||
backgroundStoryKey: string // 翻译键
|
||||
quests: CampaignQuestConfig[]
|
||||
}
|
||||
|
||||
// 战役配置
|
||||
export interface CampaignConfig {
|
||||
id: string
|
||||
nameKey: string // 翻译键
|
||||
descriptionKey: string // 翻译键
|
||||
chapters: CampaignChapterConfig[]
|
||||
}
|
||||
|
||||
// 玩家任务进度
|
||||
export interface QuestProgress {
|
||||
questId: string
|
||||
status: QuestStatus
|
||||
objectives: Record<string, { current: number; completed: boolean }>
|
||||
startedAt?: number
|
||||
completedAt?: number
|
||||
rewardsClaimed?: boolean
|
||||
dialoguesRead?: string[] // 已读对话ID
|
||||
}
|
||||
|
||||
// 玩家战役进度
|
||||
export interface PlayerCampaignProgress {
|
||||
campaignId: string
|
||||
currentChapter: number
|
||||
currentQuestId?: string
|
||||
questProgress: Record<string, QuestProgress>
|
||||
completedQuests: string[]
|
||||
unlockedQuests: string[]
|
||||
branchChoices?: Record<string, string> // 分支选择记录
|
||||
}
|
||||
|
||||
// 任务通知
|
||||
export interface QuestNotification {
|
||||
id: string
|
||||
timestamp: number
|
||||
questId: string
|
||||
questTitleKey: string // 翻译键
|
||||
eventType: 'unlocked' | 'objective_completed' | 'quest_completed' | 'chapter_completed' | 'reward_claimed'
|
||||
messageKey: string // 翻译键
|
||||
messageParams?: Record<string, string | number> // 翻译参数
|
||||
rewards?: QuestReward
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Planet, DebrisField, NPC } from '@/types/game'
|
||||
import { decryptData, encryptData } from './crypto'
|
||||
import { generatePlanetTemperature } from '@/logic/planetLogic'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
/**
|
||||
@@ -99,6 +100,41 @@ export const migrateGameData = (): void => {
|
||||
needsSave = true
|
||||
}
|
||||
|
||||
// 迁移温度数据:为没有温度的星球生成温度
|
||||
// 玩家星球
|
||||
if (oldData.player?.planets && Array.isArray(oldData.player.planets)) {
|
||||
oldData.player.planets.forEach((planet: Planet) => {
|
||||
// 月球不需要温度
|
||||
if (!planet.isMoon && !planet.temperature) {
|
||||
planet.temperature = generatePlanetTemperature(planet.position.position)
|
||||
needsSave = true
|
||||
}
|
||||
})
|
||||
if (needsSave) {
|
||||
console.log('[Migration] Added temperature to player planets')
|
||||
}
|
||||
}
|
||||
|
||||
// NPC星球
|
||||
if (oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||
let npcPlanetMigrated = false
|
||||
oldData.npcs.forEach((npc: NPC) => {
|
||||
if (npc.planets && Array.isArray(npc.planets)) {
|
||||
npc.planets.forEach((planet: Planet) => {
|
||||
// 月球不需要温度
|
||||
if (!planet.isMoon && !planet.temperature) {
|
||||
planet.temperature = generatePlanetTemperature(planet.position.position)
|
||||
needsSave = true
|
||||
npcPlanetMigrated = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (npcPlanetMigrated) {
|
||||
console.log('[Migration] Added temperature to NPC planets')
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移 player.diplomaticRelations 到 npc.relations
|
||||
// 旧版本使用 player.diplomaticRelations[npcId] 存储玩家对NPC的关系
|
||||
// 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系
|
||||
@@ -160,6 +196,10 @@ export const migrateGameData = (): void => {
|
||||
Object.entries(oldPlanets).forEach(([key, planet]) => {
|
||||
// 只迁移非玩家星球
|
||||
if (!playerPlanetIds.has(planet.id)) {
|
||||
// 为没有温度的星球生成温度
|
||||
if (!planet.isMoon && !planet.temperature) {
|
||||
planet.temperature = generatePlanetTemperature(planet.position.position)
|
||||
}
|
||||
universeData.planets[key] = planet
|
||||
}
|
||||
})
|
||||
@@ -178,6 +218,36 @@ export const migrateGameData = (): void => {
|
||||
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
||||
}
|
||||
|
||||
// 检查并更新已存在的 universeStore 数据中的星球温度
|
||||
const existingUniverseData = localStorage.getItem(universeStorageKey)
|
||||
if (existingUniverseData) {
|
||||
try {
|
||||
let universeData: { planets: Record<string, Planet>; debrisFields: Record<string, DebrisField> }
|
||||
try {
|
||||
universeData = decryptData(existingUniverseData)
|
||||
} catch {
|
||||
universeData = JSON.parse(existingUniverseData)
|
||||
}
|
||||
|
||||
let universePlanetMigrated = false
|
||||
if (universeData.planets) {
|
||||
Object.values(universeData.planets).forEach((planet: Planet) => {
|
||||
if (!planet.isMoon && !planet.temperature) {
|
||||
planet.temperature = generatePlanetTemperature(planet.position.position)
|
||||
universePlanetMigrated = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (universePlanetMigrated) {
|
||||
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
||||
console.log('[Migration] Added temperature to universe planets')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Migration] Failed to migrate universe planets temperature:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有任何数据被修改,保存gameStore数据
|
||||
if (needsSave) {
|
||||
localStorage.setItem(storageKey, encryptData(oldData))
|
||||
|
||||
@@ -9,10 +9,10 @@ export interface VersionInfo {
|
||||
// 检查GitHub最新版本
|
||||
export const checkLatestVersion = async (lastCheckTime: number, updateCheckTime: (time: number) => void): Promise<VersionInfo | null> => {
|
||||
const now = Date.now()
|
||||
const fiveMinutes = 5 * 60 * 1000 // 5分钟
|
||||
const oneHour = 60 * 60 * 1000 // 1小时
|
||||
|
||||
// 如果距离上次检查不到5分钟,跳过
|
||||
if (now - lastCheckTime < fiveMinutes) {
|
||||
// 如果距离上次检查不到1小时,跳过
|
||||
if (now - lastCheckTime < oneHour) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ export const checkLatestVersion = async (lastCheckTime: number, updateCheckTime:
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以进行版本检查(距离上次检查是否超过5分钟)
|
||||
// 检查是否可以进行版本检查(距离上次检查是否超过1小时)
|
||||
export const canCheckVersion = (lastCheckTime: number): boolean => {
|
||||
const now = Date.now()
|
||||
const fiveMinutes = 5 * 60 * 1000 // 5分钟
|
||||
return now - lastCheckTime >= fiveMinutes
|
||||
const oneHour = 60 * 60 * 1000 // 1小时
|
||||
return now - lastCheckTime >= oneHour
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
import { ShipType, DefenseType } from '@/types/game'
|
||||
import type { Fleet, BattleResult } from '@/types/game'
|
||||
import { workerManager } from '@/workers/workerManager'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import BattleReportDialog from '@/components/dialogs/BattleReportDialog.vue'
|
||||
import { Sword, Shield, Zap, RotateCcw } from 'lucide-vue-next'
|
||||
import * as planetLogic from '@/logic/planetLogic'
|
||||
|
||||
|
||||
@@ -81,20 +81,30 @@
|
||||
|
||||
<div class="text-xs sm:text-sm space-y-0.5 sm:space-y-1">
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Clock :size="14" class="flex-shrink-0" />
|
||||
<Clock :size="14" class="shrink-0" />
|
||||
<span>{{ formatTime(getBuildingTime(buildingType, getBuildingLevel(buildingType) + 1)) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Grid3x3 :size="14" class="flex-shrink-0" />
|
||||
<Grid3x3 :size="14" class="shrink-0" />
|
||||
<span>{{ BUILDINGS[buildingType].spaceUsage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 升级按钮 -->
|
||||
<Button @click="handleUpgrade(buildingType)" :disabled="!canUpgrade(buildingType)" class="w-full">
|
||||
<Button @click="handleUpgrade(buildingType, $event)" :disabled="!canUpgrade(buildingType)" class="w-full">
|
||||
{{ getUpgradeButtonText(buildingType) }}
|
||||
</Button>
|
||||
|
||||
<!-- 添加到等待队列按钮 -->
|
||||
<Button
|
||||
v-if="canAddToWaitingQueue(buildingType)"
|
||||
@click="handleAddToWaiting(buildingType, $event)"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('queue.addToWaiting') }}
|
||||
</Button>
|
||||
|
||||
<!-- 拆除按钮 -->
|
||||
<Button
|
||||
v-if="getBuildingLevel(buildingType) > 0"
|
||||
@@ -134,8 +144,8 @@
|
||||
<AlertDialogDescription v-else>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
|
||||
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||
<X v-else :size="16" class="text-red-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 shrink-0" />
|
||||
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,8 +186,8 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -195,6 +205,8 @@
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
|
||||
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
@@ -263,7 +275,7 @@
|
||||
}
|
||||
|
||||
// 升级建筑
|
||||
const handleUpgrade = (buildingType: BuildingType) => {
|
||||
const handleUpgrade = (buildingType: BuildingType, event: MouseEvent) => {
|
||||
// 检查前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||
@@ -280,6 +292,9 @@
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
||||
alertDialogShowRequirements.value = false
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
// 触发抛物线动画
|
||||
triggerQueueAnimation(event, 'building')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,12 +447,8 @@
|
||||
}
|
||||
|
||||
const handleDemolish = (buildingType: BuildingType) => {
|
||||
const buildingName = BUILDINGS.value[buildingType].name
|
||||
const refund = getDemolishRefund(buildingType)
|
||||
|
||||
demolishConfirmMessage.value = `${t('buildingsView.confirmDemolishMessage')}: ${buildingName}
|
||||
|
||||
${t('buildingsView.demolishRefund')}:
|
||||
demolishConfirmMessage.value = `${t('buildingsView.demolishRefund')}:
|
||||
${t('resources.metal')}: ${formatNumber(refund.metal)}
|
||||
${t('resources.crystal')}: ${formatNumber(refund.crystal)}
|
||||
${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
|
||||
@@ -482,4 +493,80 @@ ${t('resources.deuterium')}: ${formatNumber(refund.deuterium)}${
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
return buildingLogic.calculateDemolishRefund(buildingType, currentLevel)
|
||||
}
|
||||
|
||||
// 检查是否可以添加到等待队列
|
||||
const canAddToWaitingQueue = (buildingType: BuildingType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const config = BUILDINGS.value[buildingType]
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
|
||||
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
|
||||
const upgradesInBuildQueue = planet.value.buildQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
|
||||
const waitingQueue = planet.value.waitingBuildQueue || []
|
||||
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
|
||||
const targetLevel = currentLevel + upgradesInBuildQueue + upgradesInWaitingQueue + 1
|
||||
|
||||
// 检查是否达到等级上限(使用计算后的目标等级)
|
||||
if (config.maxLevel !== undefined && targetLevel > config.maxLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查目标等级的前置条件是否满足
|
||||
// 如果该建筑已经在队列中(正式或等待),说明基本条件已满足,跳过检查
|
||||
const alreadyInQueue = upgradesInBuildQueue > 0 || upgradesInWaitingQueue > 0
|
||||
if (!alreadyInQueue) {
|
||||
// 第一次添加时,检查当前等级+1的前置条件
|
||||
if (!checkUpgradeRequirements(buildingType)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// 后续添加时,检查目标等级的前置条件
|
||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||
if (requirements && Object.keys(requirements).length > 0) {
|
||||
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, requirements)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 建筑可以多次排队(比如金属矿升级到2、3、4、5级)
|
||||
// 只需要检查等待队列是否已满
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxWaitingQueue = waitingQueueLogic.getMaxBuildWaitingQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
if (waitingQueue.length >= maxWaitingQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 添加到等待队列
|
||||
const handleAddToWaiting = (buildingType: BuildingType, event: MouseEvent) => {
|
||||
if (!planet.value) return
|
||||
|
||||
const currentLevel = getBuildingLevel(buildingType)
|
||||
|
||||
// 计算目标等级:当前等级 + 正式队列中的升级数 + 等待队列中的升级数 + 1
|
||||
const upgradesInBuildQueue = planet.value.buildQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
|
||||
const waitingQueue = planet.value.waitingBuildQueue || []
|
||||
const upgradesInWaitingQueue = waitingQueue.filter(q => q.type === 'building' && q.itemType === buildingType).length
|
||||
const targetLevel = currentLevel + upgradesInBuildQueue + upgradesInWaitingQueue + 1
|
||||
|
||||
const item = waitingQueueLogic.createBuildingWaitingItem(buildingType, targetLevel, planet.value.id)
|
||||
|
||||
const result = waitingQueueLogic.canAddToBuildWaitingQueue(planet.value, item, gameStore.player.officers)
|
||||
if (!result.canAdd) {
|
||||
alertDialogTitle.value = t('queue.waitingQueueFull')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : ''
|
||||
alertDialogShowRequirements.value = false
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 触发抛物线动画
|
||||
triggerQueueAnimation(event, 'building')
|
||||
|
||||
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
|
||||
}
|
||||
</script>
|
||||
|
||||
454
src/views/CampaignView.vue
Normal file
454
src/views/CampaignView.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="container mx-auto px-4 py-6 max-w-6xl">
|
||||
<!-- 战役标题和总进度 -->
|
||||
<Card class="mb-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle class="flex items-center gap-2 text-xl">
|
||||
<Scroll class="h-6 w-6 text-primary" />
|
||||
{{ t('campaign.name') }}
|
||||
</CardTitle>
|
||||
<CardDescription class="mt-1">{{ t('campaign.description') }}</CardDescription>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold text-primary">{{ totalProgress }}%</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t('campaign.totalProgress') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress :model-value="totalProgress" class="h-3" />
|
||||
<div class="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||
<span>{{ completedQuestCount }} / {{ totalQuestCount }} {{ t('campaign.questsCompleted') }}</span>
|
||||
<span>{{ t('campaign.chapter') }} {{ currentChapter }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 章节选择标签 -->
|
||||
<Tabs v-model="activeChapter" class="mb-6">
|
||||
<TabsList class="grid w-full" :style="{ gridTemplateColumns: `repeat(${chapters.length}, 1fr)` }">
|
||||
<TabsTrigger
|
||||
v-for="chapter in chapters"
|
||||
:key="chapter.id"
|
||||
:value="chapter.number.toString()"
|
||||
:disabled="chapter.number > currentChapter"
|
||||
class="relative"
|
||||
>
|
||||
<span class="hidden sm:inline">{{ t(chapter.titleKey) }}</span>
|
||||
<span class="sm:hidden">{{ chapter.number }}</span>
|
||||
<Badge
|
||||
v-if="getChapterProgress(chapter.number) === 100"
|
||||
variant="default"
|
||||
class="absolute -top-1 -right-1 h-4 w-4 p-0 flex items-center justify-center text-[10px]"
|
||||
>
|
||||
<Check class="h-3 w-3" />
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 章节内容 -->
|
||||
<TabsContent v-for="chapter in chapters" :key="chapter.id" :value="chapter.number.toString()" class="mt-4">
|
||||
<!-- 章节背景故事 -->
|
||||
<Card class="mb-4 bg-gradient-to-r from-primary/5 to-transparent">
|
||||
<CardContent class="py-4">
|
||||
<p class="text-sm text-muted-foreground italic">{{ t(chapter.backgroundStoryKey) }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 任务地图 -->
|
||||
<QuestMap
|
||||
:quests="getChapterQuests(chapter.number)"
|
||||
:progress="campaignProgress"
|
||||
@select-quest="handleQuestSelect"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 任务详情面板 -->
|
||||
<Card v-if="selectedQuest" class="mt-6">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
:class="[
|
||||
'h-10 w-10 rounded-full flex items-center justify-center',
|
||||
getQuestStatusClass(selectedQuest.id)
|
||||
]"
|
||||
>
|
||||
<component :is="getQuestStatusIcon(selectedQuest.id)" class="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
{{ t(selectedQuest.titleKey) }}
|
||||
<Badge v-if="selectedQuest.isBoss" variant="destructive">BOSS</Badge>
|
||||
<Badge v-if="selectedQuest.isBranch" variant="secondary">{{ t('campaign.branch') }}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>{{ t(selectedQuest.descriptionKey) }}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="canStartQuest(selectedQuest.id)"
|
||||
@click="handleStartQuest(selectedQuest.id)"
|
||||
>
|
||||
<Play class="h-4 w-4 mr-2" />
|
||||
{{ t('campaign.startQuest') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="canClaimRewards(selectedQuest.id)"
|
||||
@click="handleClaimRewards(selectedQuest.id)"
|
||||
variant="default"
|
||||
>
|
||||
<Gift class="h-4 w-4 mr-2" />
|
||||
{{ t('campaign.claimRewards') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<!-- 任务目标 -->
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<Target class="h-4 w-4" />
|
||||
{{ t('campaign.objectives') }}
|
||||
</h4>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="objective in selectedQuest.objectives"
|
||||
:key="objective.id"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'h-6 w-6 rounded-full flex items-center justify-center text-xs',
|
||||
isObjectiveCompleted(selectedQuest.id, objective.id)
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
]"
|
||||
>
|
||||
<Check v-if="isObjectiveCompleted(selectedQuest.id, objective.id)" class="h-4 w-4" />
|
||||
<span v-else>{{ getObjectiveProgress(selectedQuest.id, objective.id) }}</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm">{{ t(objective.descriptionKey) }}</div>
|
||||
<Progress
|
||||
:model-value="(getObjectiveProgress(selectedQuest.id, objective.id) / objective.required) * 100"
|
||||
class="h-1.5 mt-1"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ getObjectiveProgress(selectedQuest.id, objective.id) }} / {{ objective.required }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务奖励 -->
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<Gift class="h-4 w-4" />
|
||||
{{ t('campaign.rewards') }}
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Badge v-if="selectedQuest.rewards.resources?.metal" variant="outline" class="gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(selectedQuest.rewards.resources.metal) }}
|
||||
</Badge>
|
||||
<Badge v-if="selectedQuest.rewards.resources?.crystal" variant="outline" class="gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(selectedQuest.rewards.resources.crystal) }}
|
||||
</Badge>
|
||||
<Badge v-if="selectedQuest.rewards.resources?.deuterium" variant="outline" class="gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(selectedQuest.rewards.resources.deuterium) }}
|
||||
</Badge>
|
||||
<Badge v-if="selectedQuest.rewards.darkMatter" variant="outline" class="gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(selectedQuest.rewards.darkMatter) }}
|
||||
</Badge>
|
||||
<Badge v-if="selectedQuest.rewards.points" variant="secondary" class="gap-1">
|
||||
<Star class="h-3 w-3" />
|
||||
+{{ formatNumber(selectedQuest.rewards.points) }} {{ t('common.points') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-for="(count, shipType) in selectedQuest.rewards.ships"
|
||||
:key="shipType"
|
||||
variant="outline"
|
||||
class="gap-1"
|
||||
>
|
||||
<Rocket class="h-3 w-3" />
|
||||
{{ count }}x {{ getShipName(shipType) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 剧情对话框 -->
|
||||
<StoryDialog
|
||||
v-if="showStoryDialog"
|
||||
:dialogues="currentDialogues"
|
||||
@close="handleDialogueClose"
|
||||
@choice="handleDialogueChoice"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import QuestMap from '@/components/campaign/QuestMap.vue'
|
||||
import StoryDialog from '@/components/campaign/StoryDialog.vue'
|
||||
import {
|
||||
Scroll,
|
||||
Check,
|
||||
Play,
|
||||
Gift,
|
||||
Target,
|
||||
Star,
|
||||
Rocket,
|
||||
Lock,
|
||||
Circle,
|
||||
CheckCircle2
|
||||
} from 'lucide-vue-next'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { MAIN_CAMPAIGN, getQuestsByChapter, getQuestById, getTotalQuestCount } from '@/config/campaignConfig'
|
||||
import * as campaignLogic from '@/logic/campaignLogic'
|
||||
import { QuestStatus, type CampaignQuestConfig, type StoryDialogue } from '@/types/game'
|
||||
import { SHIPS } from '@/config/gameConfig'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const { t } = useI18n()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
|
||||
// 初始化战役进度
|
||||
onMounted(() => {
|
||||
if (!gameStore.player.campaignProgress) {
|
||||
gameStore.player.campaignProgress = campaignLogic.initializeCampaignProgress(gameStore.player)
|
||||
}
|
||||
})
|
||||
|
||||
// 响应式状态
|
||||
const activeChapter = ref('1')
|
||||
const selectedQuestId = ref<string | null>(null)
|
||||
const showStoryDialog = ref(false)
|
||||
const currentDialogues = ref<StoryDialogue[]>([])
|
||||
const pendingAction = ref<'start' | 'claim' | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const chapters = computed(() => MAIN_CAMPAIGN.chapters)
|
||||
|
||||
const campaignProgress = computed(() => gameStore.player.campaignProgress)
|
||||
|
||||
const currentChapter = computed(() => campaignProgress.value?.currentChapter || 1)
|
||||
|
||||
const totalProgress = computed(() => {
|
||||
if (!campaignProgress.value) return 0
|
||||
return campaignLogic.calculateCampaignProgress(campaignProgress.value)
|
||||
})
|
||||
|
||||
const totalQuestCount = computed(() => getTotalQuestCount())
|
||||
|
||||
const completedQuestCount = computed(() => campaignProgress.value?.completedQuests.length || 0)
|
||||
|
||||
const selectedQuest = computed(() => {
|
||||
if (!selectedQuestId.value) return null
|
||||
return getQuestById(selectedQuestId.value)
|
||||
})
|
||||
|
||||
// 获取章节任务
|
||||
const getChapterQuests = (chapterNumber: number): CampaignQuestConfig[] => {
|
||||
return getQuestsByChapter(chapterNumber)
|
||||
}
|
||||
|
||||
// 获取章节进度
|
||||
const getChapterProgress = (chapterNumber: number): number => {
|
||||
if (!campaignProgress.value) return 0
|
||||
return campaignLogic.calculateChapterProgress(campaignProgress.value, chapterNumber)
|
||||
}
|
||||
|
||||
// 获取任务状态
|
||||
const getQuestStatus = (questId: string): QuestStatus => {
|
||||
if (!campaignProgress.value) return QuestStatus.Locked
|
||||
return campaignLogic.getQuestStatus(campaignProgress.value, questId)
|
||||
}
|
||||
|
||||
// 获取任务状态样式
|
||||
const getQuestStatusClass = (questId: string): string => {
|
||||
const status = getQuestStatus(questId)
|
||||
switch (status) {
|
||||
case QuestStatus.Completed:
|
||||
return 'bg-green-500 text-white'
|
||||
case QuestStatus.Active:
|
||||
return 'bg-primary text-primary-foreground'
|
||||
case QuestStatus.Available:
|
||||
return 'bg-blue-500 text-white'
|
||||
default:
|
||||
return 'bg-muted text-muted-foreground'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务状态图标
|
||||
const getQuestStatusIcon = (questId: string) => {
|
||||
const status = getQuestStatus(questId)
|
||||
switch (status) {
|
||||
case QuestStatus.Completed:
|
||||
return CheckCircle2
|
||||
case QuestStatus.Active:
|
||||
return Circle
|
||||
case QuestStatus.Available:
|
||||
return Circle
|
||||
default:
|
||||
return Lock
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以开始任务
|
||||
const canStartQuest = (questId: string): boolean => {
|
||||
const status = getQuestStatus(questId)
|
||||
return status === QuestStatus.Available
|
||||
}
|
||||
|
||||
// 检查是否可以领取奖励
|
||||
const canClaimRewards = (questId: string): boolean => {
|
||||
const status = getQuestStatus(questId)
|
||||
const progress = campaignProgress.value?.questProgress[questId]
|
||||
return status === QuestStatus.Completed && !progress?.rewardsClaimed
|
||||
}
|
||||
|
||||
// 检查目标是否完成
|
||||
const isObjectiveCompleted = (questId: string, objectiveId: string): boolean => {
|
||||
const progress = campaignProgress.value?.questProgress[questId]
|
||||
return progress?.objectives[objectiveId]?.completed || false
|
||||
}
|
||||
|
||||
// 获取目标进度
|
||||
const getObjectiveProgress = (questId: string, objectiveId: string): number => {
|
||||
const progress = campaignProgress.value?.questProgress[questId]
|
||||
return progress?.objectives[objectiveId]?.current || 0
|
||||
}
|
||||
|
||||
// 获取舰船名称
|
||||
const getShipName = (shipType: string): string => {
|
||||
const ship = SHIPS[shipType as keyof typeof SHIPS]
|
||||
return ship?.name || shipType
|
||||
}
|
||||
|
||||
// 处理任务选择
|
||||
const handleQuestSelect = (questId: string) => {
|
||||
selectedQuestId.value = questId
|
||||
}
|
||||
|
||||
// 处理开始任务
|
||||
const handleStartQuest = (questId: string) => {
|
||||
const quest = getQuestById(questId)
|
||||
|
||||
// 如果有开场对话,先显示对话
|
||||
if (quest?.prologueDialogues && quest.prologueDialogues.length > 0) {
|
||||
currentDialogues.value = quest.prologueDialogues
|
||||
pendingAction.value = 'start'
|
||||
showStoryDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 直接开始任务
|
||||
executeStartQuest(questId)
|
||||
}
|
||||
|
||||
// 执行开始任务
|
||||
const executeStartQuest = (questId: string) => {
|
||||
const result = campaignLogic.startQuest(gameStore.player, questId)
|
||||
if (result.success) {
|
||||
toast.success(t('campaign.notifications.questStarted'))
|
||||
// 立即检查进度
|
||||
campaignLogic.checkAllActiveQuestsProgress(gameStore.player, npcStore.npcs)
|
||||
} else if (result.error) {
|
||||
toast.error(t(result.error))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理领取奖励
|
||||
const handleClaimRewards = (questId: string) => {
|
||||
const quest = getQuestById(questId)
|
||||
|
||||
// 如果有结束对话,先显示对话
|
||||
if (quest?.epilogueDialogues && quest.epilogueDialogues.length > 0) {
|
||||
currentDialogues.value = quest.epilogueDialogues
|
||||
pendingAction.value = 'claim'
|
||||
showStoryDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 直接领取奖励
|
||||
executeClaimRewards(questId)
|
||||
}
|
||||
|
||||
// 执行领取奖励
|
||||
const executeClaimRewards = (questId: string) => {
|
||||
const result = campaignLogic.claimQuestRewards(gameStore.player, questId)
|
||||
if (result.success) {
|
||||
toast.success(t('campaign.notifications.rewardsClaimed'))
|
||||
} else if (result.error) {
|
||||
toast.error(t(result.error))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理对话关闭
|
||||
const handleDialogueClose = () => {
|
||||
showStoryDialog.value = false
|
||||
|
||||
if (pendingAction.value && selectedQuestId.value) {
|
||||
if (pendingAction.value === 'start') {
|
||||
executeStartQuest(selectedQuestId.value)
|
||||
} else if (pendingAction.value === 'claim') {
|
||||
executeClaimRewards(selectedQuestId.value)
|
||||
}
|
||||
}
|
||||
|
||||
pendingAction.value = null
|
||||
currentDialogues.value = []
|
||||
}
|
||||
|
||||
// 处理对话选项选择
|
||||
const handleDialogueChoice = (choice: { effect?: string }) => {
|
||||
// TODO: 处理选择效果
|
||||
console.log('Dialogue choice:', choice)
|
||||
}
|
||||
|
||||
// 监听章节变化,自动选择第一个可用任务
|
||||
watch(activeChapter, (newChapter) => {
|
||||
const chapterQuests = getChapterQuests(parseInt(newChapter))
|
||||
const availableQuest = chapterQuests.find(quest => {
|
||||
const status = getQuestStatus(quest.id)
|
||||
return status === QuestStatus.Active || status === QuestStatus.Available
|
||||
})
|
||||
if (availableQuest) {
|
||||
selectedQuestId.value = availableQuest.id
|
||||
} else {
|
||||
const firstQuest = chapterQuests[0]
|
||||
if (firstQuest) {
|
||||
selectedQuestId.value = firstQuest.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 初始选择当前任务
|
||||
onMounted(() => {
|
||||
if (campaignProgress.value?.currentQuestId) {
|
||||
selectedQuestId.value = campaignProgress.value.currentQuestId
|
||||
const quest = getQuestById(campaignProgress.value.currentQuestId)
|
||||
if (quest) {
|
||||
activeChapter.value = quest.chapter.toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -127,9 +127,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button @click="handleBuild(defenseType)" :disabled="!canBuild(defenseType)" class="w-full">
|
||||
<Button @click="handleBuild(defenseType, $event)" :disabled="!canBuild(defenseType)" class="w-full">
|
||||
{{ t('defenseView.build') }}
|
||||
</Button>
|
||||
|
||||
<!-- 添加到等待队列按钮 -->
|
||||
<Button
|
||||
v-if="canAddToWaitingQueue(defenseType)"
|
||||
@click="handleAddToWaiting(defenseType, $event)"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
>
|
||||
{{ t('queue.addToWaiting') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -164,7 +174,7 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -174,13 +184,16 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
|
||||
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
|
||||
import CardUnlockOverlay from '@/components/common/CardUnlockOverlay.vue'
|
||||
import { formatNumber, getResourceCostColor } from '@/utils/format'
|
||||
import * as publicLogic from '@/logic/publicLogic'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
import * as shipLogic from '@/logic/shipLogic'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as waitingQueueLogic from '@/logic/waitingQueueLogic'
|
||||
import * as officerLogic from '@/logic/officerLogic'
|
||||
import { triggerQueueAnimation } from '@/composables/useQueueAnimation'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const detailDialog = useDetailDialogStore()
|
||||
@@ -248,7 +261,7 @@
|
||||
}
|
||||
|
||||
// 建造防御设施
|
||||
const handleBuild = (defenseType: DefenseType) => {
|
||||
const handleBuild = (defenseType: DefenseType, event: MouseEvent) => {
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) {
|
||||
alertDialogTitle.value = t('defenseView.inputError')
|
||||
@@ -263,6 +276,8 @@
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : t('defenseView.buildFailedMessage')
|
||||
alertDialogOpen.value = true
|
||||
} else {
|
||||
// 触发抛物线动画
|
||||
triggerQueueAnimation(event, 'defense')
|
||||
quantities.value[defenseType] = 0
|
||||
}
|
||||
}
|
||||
@@ -308,4 +323,59 @@
|
||||
darkMatter: config.cost.darkMatter * quantity
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以添加到等待队列
|
||||
const canAddToWaitingQueue = (defenseType: DefenseType): boolean => {
|
||||
if (!planet.value) return false
|
||||
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) return false
|
||||
|
||||
// 护盾罩只能建造一个
|
||||
if (isShieldDome(defenseType)) {
|
||||
if (planet.value.defense[defenseType] > 0) return false
|
||||
if (quantity > 1) return false
|
||||
}
|
||||
|
||||
// 检查前置条件是否满足
|
||||
const config = DEFENSES.value[defenseType]
|
||||
if (!publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查等待队列是否已满
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const maxWaitingQueue = waitingQueueLogic.getMaxBuildWaitingQueue(planet.value, bonuses.additionalBuildQueue)
|
||||
const waitingQueue = planet.value.waitingBuildQueue || []
|
||||
if (waitingQueue.length >= maxWaitingQueue) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只有当建造按钮被禁用时(资源不足)才显示等待队列按钮
|
||||
return !canBuild(defenseType)
|
||||
}
|
||||
|
||||
// 添加到等待队列
|
||||
const handleAddToWaiting = (defenseType: DefenseType, event: MouseEvent) => {
|
||||
if (!planet.value) return
|
||||
|
||||
const quantity = quantities.value[defenseType]
|
||||
if (quantity <= 0) return
|
||||
|
||||
const item = waitingQueueLogic.createDefenseWaitingItem(defenseType, quantity, planet.value.id)
|
||||
|
||||
const result = waitingQueueLogic.canAddToBuildWaitingQueue(planet.value, item, gameStore.player.officers)
|
||||
if (!result.canAdd) {
|
||||
alertDialogTitle.value = t('queue.waitingQueueFull')
|
||||
alertDialogMessage.value = result.reason ? t(result.reason) : ''
|
||||
alertDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 触发抛物线动画
|
||||
triggerQueueAnimation(event, 'defense')
|
||||
|
||||
waitingQueueLogic.addToBuildWaitingQueue(planet.value, item)
|
||||
quantities.value[defenseType] = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||
</div>
|
||||
<!-- 视图切换和诊断按钮 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<!-- 视图模式切换 -->
|
||||
<div class="flex items-center border rounded-md">
|
||||
<Button
|
||||
@@ -79,6 +79,12 @@
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.difficulty') }}:</span>
|
||||
<span class="font-medium">{{ t(`diplomacy.diagnostic.difficultyLevels.${diagnostic.difficulty}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.aiType') }}:</span>
|
||||
<span class="font-medium" :title="diagnostic.aiType ? t(`diplomacy.diagnostic.aiTypeDescriptions.${diagnostic.aiType}`) : ''">
|
||||
{{ diagnostic.aiType ? t(`diplomacy.diagnostic.aiTypes.${diagnostic.aiType}`) : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.reputation') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||
@@ -381,8 +387,8 @@
|
||||
PaginationPrevious
|
||||
} from '@/components/ui/pagination'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import NpcRelationRow from '@/components/NpcRelationRow.vue'
|
||||
import NpcRelationCard from '@/components/npc/NpcRelationCard.vue'
|
||||
import NpcRelationRow from '@/components/npc/NpcRelationRow.vue'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import type { DiplomaticRelation } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-4' : 'grid-cols-3']">
|
||||
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-3' : 'grid-cols-2']">
|
||||
<TabsTrigger v-for="tab in visibleTabs" :key="tab.value" :value="tab.value">
|
||||
{{ t(`fleetView.${tab.labelKey}`) }}
|
||||
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
@@ -17,37 +17,6 @@
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 舰队总览 -->
|
||||
<TabsContent value="fleet" class="mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.currentPlanetFleet') }}</CardTitle>
|
||||
<CardDescription>
|
||||
{{ planet.name }} [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="p-3 sm:p-4 border rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm sm:text-base">{{ SHIPS[shipType].name }}</h3>
|
||||
<p class="text-xl sm:text-2xl font-bold">{{ formatNumber(count) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs sm:text-sm text-muted-foreground space-y-1">
|
||||
<p>{{ t('fleetView.attack') }}: {{ SHIPS[shipType].attack }}</p>
|
||||
<p>{{ t('fleetView.shield') }}: {{ SHIPS[shipType].shield }}</p>
|
||||
<p>{{ t('fleetView.armor') }}: {{ SHIPS[shipType].armor }}</p>
|
||||
<p>{{ t('fleetView.speed') }}: {{ formatNumber(SHIPS[shipType].speed) }}</p>
|
||||
<p>{{ t('fleetView.cargo') }}: {{ formatNumber(SHIPS[shipType].cargoCapacity) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 派遣舰队 -->
|
||||
<TabsContent value="send" class="mt-4 space-y-4">
|
||||
<!-- 舰队任务槽位信息 -->
|
||||
@@ -213,6 +182,47 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 探险区域选择(仅探险任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Expedition">
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('fleetView.expeditionZone') }}</CardTitle>
|
||||
<CardDescription>{{ t('fleetView.expeditionZoneDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Button
|
||||
v-for="item in availableExpeditionZones"
|
||||
:key="item.zone"
|
||||
@click="item.unlocked && (selectedExpeditionZone = item.zone)"
|
||||
variant="outline"
|
||||
:disabled="!item.unlocked"
|
||||
:class="[
|
||||
'h-auto py-3 flex flex-col items-start text-left',
|
||||
selectedExpeditionZone === item.zone ? 'ring-2 ring-primary' : ''
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<span class="font-medium">{{ t(`fleetView.zones.${item.zone}.name`) }}</span>
|
||||
<Badge v-if="!item.unlocked" variant="secondary" class="ml-auto text-xs">
|
||||
{{ t('fleetView.requiresAstro', { level: item.config.requiredTechLevel }) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">
|
||||
{{ t(`fleetView.zones.${item.zone}.desc`) }}
|
||||
</div>
|
||||
<div class="flex gap-3 mt-2 text-xs">
|
||||
<span :class="item.config.resourceMultiplier > 1 ? 'text-green-500' : ''">
|
||||
{{ t('fleetView.reward') }}: x{{ item.config.resourceMultiplier }}
|
||||
</span>
|
||||
<span :class="item.config.dangerMultiplier > 1 ? 'text-red-500' : 'text-green-500'">
|
||||
{{ t('fleetView.danger') }}: x{{ item.config.dangerMultiplier }}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运输资源(仅运输任务) -->
|
||||
<Card v-if="selectedMission === MissionType.Transport">
|
||||
<CardHeader>
|
||||
@@ -298,7 +308,12 @@
|
||||
<CardHeader>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionName(mission.missionType) }}</CardTitle>
|
||||
<CardTitle class="text-base sm:text-lg flex items-center gap-2">
|
||||
{{ getMissionName(mission.missionType) }}
|
||||
<Badge v-if="mission.missionType === MissionType.Expedition && mission.expeditionZone" variant="outline" class="text-xs">
|
||||
{{ t(`fleetView.zones.${mission.expeditionZone}.name`) }}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ getPlanetName(mission.originPlanetId) }} → [{{ mission.targetPosition.galaxy }}:{{ mission.targetPosition.system }}:{{
|
||||
mission.targetPosition.position
|
||||
@@ -510,7 +525,12 @@
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
@click="() => { showPresetNameDialog = false; pendingPresetAction = null }"
|
||||
@click="
|
||||
() => {
|
||||
showPresetNameDialog = false
|
||||
pendingPresetAction = null
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ t('common.cancel') }}
|
||||
</AlertDialogCancel>
|
||||
@@ -531,8 +551,9 @@
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
|
||||
import { ShipType, MissionType, BuildingType, TechnologyType, ExpeditionZone } from '@/types/game'
|
||||
import type { Fleet, Resources, FleetPreset } from '@/types/game'
|
||||
import { EXPEDITION_ZONES } from '@/config/gameConfig'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -541,7 +562,7 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -552,7 +573,7 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import UnlockRequirement from '@/components/common/UnlockRequirement.vue'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import {
|
||||
Sword,
|
||||
@@ -610,11 +631,10 @@
|
||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||
})
|
||||
|
||||
const activeTab = ref<'fleet' | 'send' | 'missions' | 'jumpGate'>('fleet')
|
||||
const activeTab = ref<'send' | 'missions' | 'jumpGate'>('send')
|
||||
|
||||
// Tab 配置
|
||||
const fleetTabs = [
|
||||
{ value: 'fleet', labelKey: 'fleetOverview' },
|
||||
{ value: 'send', labelKey: 'sendFleet' },
|
||||
{ value: 'missions', labelKey: 'flightMissions' },
|
||||
{ value: 'jumpGate', labelKey: 'jumpGate' }
|
||||
@@ -773,6 +793,23 @@
|
||||
// 选择的任务类型
|
||||
const selectedMission = ref<MissionType>(MissionType.Attack)
|
||||
|
||||
// 探险区域选择
|
||||
const selectedExpeditionZone = ref<ExpeditionZone>(ExpeditionZone.NearSpace)
|
||||
|
||||
// 获取玩家的天体物理学等级
|
||||
const astrophysicsLevel = computed(() => {
|
||||
return gameStore.player.technologies[TechnologyType.Astrophysics] || 0
|
||||
})
|
||||
|
||||
// 可用的探险区域(基于天体物理学等级)
|
||||
const availableExpeditionZones = computed(() => {
|
||||
return Object.values(ExpeditionZone).map(zone => ({
|
||||
zone,
|
||||
config: EXPEDITION_ZONES[zone],
|
||||
unlocked: astrophysicsLevel.value >= EXPEDITION_ZONES[zone].requiredTechLevel
|
||||
}))
|
||||
})
|
||||
|
||||
// 运输资源
|
||||
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
||||
|
||||
@@ -1127,7 +1164,15 @@
|
||||
const distance = fleetLogic.calculateDistance(planet.value.position, targetPosition.value)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const minSpeed = shipLogic.calculateFleetMinSpeed(selectedFleet.value, bonuses.fleetSpeedBonus)
|
||||
return fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
let flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
|
||||
// 探险任务应用区域飞行时间倍率
|
||||
if (selectedMission.value === MissionType.Expedition) {
|
||||
const zoneConfig = EXPEDITION_ZONES[selectedExpeditionZone.value]
|
||||
flightTime = Math.floor(flightTime * zoneConfig.flightTimeMultiplier)
|
||||
}
|
||||
|
||||
return flightTime
|
||||
}
|
||||
|
||||
// 检查是否可以派遣
|
||||
@@ -1219,7 +1264,14 @@
|
||||
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
const minSpeed = shipLogic.calculateFleetMinSpeed(fleet, bonuses.fleetSpeedBonus)
|
||||
const flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
let flightTime = fleetLogic.calculateFlightTime(distance, minSpeed)
|
||||
|
||||
// 探险任务应用区域飞行时间倍率
|
||||
if (missionType === MissionType.Expedition) {
|
||||
const zoneConfig = EXPEDITION_ZONES[selectedExpeditionZone.value]
|
||||
flightTime = Math.floor(flightTime * zoneConfig.flightTimeMultiplier)
|
||||
}
|
||||
|
||||
const mission = fleetLogic.createFleetMission(
|
||||
gameStore.player.id,
|
||||
gameStore.currentPlanet.id,
|
||||
@@ -1241,6 +1293,11 @@
|
||||
mission.giftTargetNpcId = targetNpc.value.id
|
||||
}
|
||||
|
||||
// 如果是探险任务,设置探险区域
|
||||
if (missionType === MissionType.Expedition) {
|
||||
mission.expeditionZone = selectedExpeditionZone.value
|
||||
}
|
||||
|
||||
gameStore.player.fleetMissions.push(mission)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<Globe class="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5 mb-0.5">
|
||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
||||
@@ -134,7 +134,7 @@
|
||||
size="sm"
|
||||
>
|
||||
<div class="flex items-start gap-2 w-full min-w-0">
|
||||
<Globe class="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<Globe class="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate font-medium text-sm mb-0.5">{{ p.name }}</div>
|
||||
<div class="text-[11px] text-muted-foreground">
|
||||
@@ -197,7 +197,7 @@
|
||||
<!-- 第一行:位置编号 + 星球信息(名称、坐标、状态、残骸) -->
|
||||
<div class="flex items-start gap-2 w-full">
|
||||
<!-- 位置编号 -->
|
||||
<div class="w-8 text-center flex-shrink-0">
|
||||
<div class="w-8 text-center shrink-0">
|
||||
<Badge variant="outline" class="text-xs">{{ slot.position }}</Badge>
|
||||
</div>
|
||||
<!-- 星球信息 -->
|
||||
@@ -208,15 +208,15 @@
|
||||
<h3 class="font-semibold text-sm truncate">
|
||||
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap shrink-0">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</span>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs shrink-0">
|
||||
{{ t('galaxyView.mine') }}
|
||||
</Badge>
|
||||
<Popover v-else>
|
||||
<PopoverTrigger as-child>
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0 cursor-pointer">
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs shrink-0 cursor-pointer">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
@@ -233,7 +233,7 @@
|
||||
<Badge
|
||||
v-if="getNpcDifficultyLevel(slot.planet) !== null"
|
||||
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
|
||||
class="text-xs flex-shrink-0"
|
||||
class="text-xs shrink-0"
|
||||
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
|
||||
>
|
||||
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
|
||||
@@ -269,6 +269,54 @@
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<!-- 矿脉储量徽章 -->
|
||||
<Popover v-if="getOreDeposits(slot.planet)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-emerald-50 dark:hover:bg-emerald-950/30 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400 gap-1"
|
||||
>
|
||||
<Mountain class="h-3 w-3" />
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="center">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-emerald-700 dark:text-emerald-400">{{ t('galaxyView.oreDeposits') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<!-- 月球徽章 -->
|
||||
<Badge
|
||||
v-if="slot.moon"
|
||||
@@ -416,7 +464,7 @@
|
||||
<!-- PC端布局:位置编号 + 星球信息(水平) -->
|
||||
<div class="hidden sm:flex items-center gap-4 flex-1 min-w-0">
|
||||
<!-- 位置编号 -->
|
||||
<div class="w-12 text-center flex-shrink-0">
|
||||
<div class="w-12 text-center shrink-0">
|
||||
<Badge variant="outline" class="text-sm">{{ slot.position }}</Badge>
|
||||
</div>
|
||||
|
||||
@@ -488,6 +536,55 @@
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<!-- 矿脉储量徽章 -->
|
||||
<Popover v-if="getOreDeposits(slot.planet)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-xs cursor-pointer hover:bg-emerald-50 dark:hover:bg-emerald-950/30 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400 gap-1"
|
||||
>
|
||||
<Mountain class="h-3 w-3" />
|
||||
<span>{{ t('galaxyView.deposits') }}</span>
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-emerald-700 dark:text-emerald-400">{{ t('galaxyView.oreDeposits') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'metal') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.metal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'crystal') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.crystal) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'depleted' ? 'text-destructive' : getDepositStatus(getOreDeposits(slot.planet)!, 'deuterium') === 'warning' ? 'text-yellow-600' : ''"
|
||||
>
|
||||
{{ formatDepositShort(getOreDeposits(slot.planet)!.deuterium) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<!-- 月球徽章 -->
|
||||
<Badge
|
||||
v-if="slot.moon"
|
||||
@@ -545,7 +642,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 (PC端) -->
|
||||
<div class="hidden sm:flex gap-1 sm:gap-2 flex-shrink-0">
|
||||
<div class="hidden sm:flex gap-1 sm:gap-2 shrink-0">
|
||||
<TooltipProvider :delay-duration="300">
|
||||
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet)">
|
||||
<TooltipTrigger as-child>
|
||||
@@ -829,14 +926,15 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar } from 'lucide-vue-next'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar, Mountain } from 'lucide-vue-next'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import * as gameLogic from '@/logic/gameLogic'
|
||||
import * as moonLogic from '@/logic/moonLogic'
|
||||
import * as oreDepositLogic from '@/logic/oreDepositLogic'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import { BuildingType, MissionType } from '@/types/game'
|
||||
import type { FleetMission } from '@/types/game'
|
||||
import type { FleetMission, OreDeposits } from '@/types/game'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
@@ -957,6 +1055,27 @@
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 获取星球的矿脉储量信息
|
||||
const getOreDeposits = (planet: Planet | null): OreDeposits | null => {
|
||||
if (!planet || planet.isMoon) return null
|
||||
return planet.oreDeposits || null
|
||||
}
|
||||
|
||||
// 格式化矿脉储量(短格式)
|
||||
const formatDepositShort = (value: number): string => {
|
||||
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`
|
||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
|
||||
if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`
|
||||
return String(Math.floor(value))
|
||||
}
|
||||
|
||||
// 获取矿脉储量百分比对应的颜色状态
|
||||
const getDepositStatus = (deposits: OreDeposits, resourceType: 'metal' | 'crystal' | 'deuterium'): 'normal' | 'warning' | 'depleted' => {
|
||||
if (oreDepositLogic.isDepositDepleted(deposits, resourceType)) return 'depleted'
|
||||
if (oreDepositLogic.isDepositWarning(deposits, resourceType)) return 'warning'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Rocket, Languages, Shield } from 'lucide-vue-next'
|
||||
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||
import PrivacyDialog from '@/components/dialogs/PrivacyDialog.vue'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -62,14 +62,14 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Sword class="h-4 w-4 flex-shrink-0" />
|
||||
<Sword class="h-4 w-4 shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.battleReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="getBattleResultVariant(report)" class="text-xs">
|
||||
{{ getBattleResultText(report) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteBattleReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -99,12 +99,12 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Eye class="h-4 w-4 flex-shrink-0" />
|
||||
<Eye class="h-4 w-4 shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spyReport') }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ report.targetPlanetId }}</Badge>
|
||||
<Badge variant="outline" class="text-xs">{{ getSpyReportTargetName(report) }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteSpyReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -124,19 +124,20 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<AlertTriangle class="h-4 w-4 flex-shrink-0 text-destructive" />
|
||||
<AlertTriangle class="h-4 w-4 shrink-0 text-destructive" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.spiedNotification') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="notification.detectionSuccess ? 'destructive' : 'secondary'" class="text-xs">
|
||||
{{ notification.detectionSuccess ? t('messagesView.detected') : t('messagesView.undetected') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteSpiedNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteSpiedNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} → {{ notification.targetPlanetName }} · {{ formatDate(notification.timestamp) }}
|
||||
{{ getNpcName(notification.npcId, notification.npcName) }} → {{ notification.targetPlanetName }} ·
|
||||
{{ formatDate(notification.timestamp) }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -168,21 +169,16 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Recycle class="h-4 w-4 flex-shrink-0 text-blue-500" />
|
||||
<Recycle class="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.npcRecycleActivity') }}</CardTitle>
|
||||
<Badge v-if="!notification.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
@click.stop="deleteNPCActivityNotification(notification.id)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 flex-shrink-0"
|
||||
>
|
||||
<Button @click.stop="deleteNPCActivityNotification(notification.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription class="text-xs sm:text-sm">
|
||||
{{ notification.npcName }} →
|
||||
{{ getNpcName(notification.npcId, notification.npcName) }} →
|
||||
{{
|
||||
notification.targetPlanetName ||
|
||||
`[${notification.targetPosition.galaxy}:${notification.targetPosition.system}:${notification.targetPosition.position}]`
|
||||
@@ -202,11 +198,13 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Gift class="h-4 w-4 flex-shrink-0 text-green-600" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ t('messagesView.giftFrom').replace('{npcName}', gift.fromNpcName) }}</CardTitle>
|
||||
<Gift class="h-4 w-4 shrink-0 text-green-600" />
|
||||
<CardTitle class="text-base sm:text-lg">
|
||||
{{ t('messagesView.giftFrom').replace('{npcName}', getNpcName(gift.fromNpcId, gift.fromNpcName)) }}
|
||||
</CardTitle>
|
||||
<Badge v-if="!gift.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftNotification(gift.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteGiftNotification(gift.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -252,13 +250,13 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Ban class="h-4 w-4 flex-shrink-0 text-red-600" />
|
||||
<Ban class="h-4 w-4 shrink-0 text-red-600" />
|
||||
<CardTitle class="text-base sm:text-lg">
|
||||
{{ t('messagesView.giftRejectedBy').replace('{npcName}', rejection.npcName) }}
|
||||
{{ t('messagesView.giftRejectedBy').replace('{npcName}', getNpcName(rejection.npcId, rejection.npcName)) }}
|
||||
</CardTitle>
|
||||
<Badge v-if="!rejection.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteGiftRejectedNotification(rejection.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteGiftRejectedNotification(rejection.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -306,14 +304,14 @@
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Package class="h-4 w-4 flex-shrink-0" />
|
||||
<Package class="h-4 w-4 shrink-0" />
|
||||
<CardTitle class="text-base sm:text-lg">{{ getMissionTypeName(report.missionType) }}</CardTitle>
|
||||
<Badge v-if="!report.read" variant="default" class="text-xs">{{ t('messagesView.unread') }}</Badge>
|
||||
<Badge :variant="report.success ? 'default' : 'destructive'" class="text-xs">
|
||||
{{ report.success ? t('messagesView.success') : t('messagesView.failed') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button @click.stop="deleteMissionReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 flex-shrink-0">
|
||||
<Button @click.stop="deleteMissionReport(report.id)" variant="ghost" size="icon" class="h-8 w-8 shrink-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -339,322 +337,14 @@
|
||||
<!-- 间谍报告对话框 -->
|
||||
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
|
||||
|
||||
<!-- 被侦查通知详情对话框 -->
|
||||
<Dialog :open="showSpiedDialog" @update:open="showSpiedDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Eye class="h-5 w-5 text-purple-500" />
|
||||
{{ t('messagesView.spiedNotificationDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.spyDetected') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<!-- 被侦查通知对话框 -->
|
||||
<SpiedNotificationDialog v-model:open="showSpiedDialog" :notification="selectedSpiedNotification" />
|
||||
|
||||
<div v-if="selectedSpiedNotification" class="space-y-4">
|
||||
<!-- 侦查者信息 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ selectedSpiedNotification.npcName }}</h3>
|
||||
<Badge variant="destructive">{{ t('messagesView.spyDetected') }}</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedSpiedNotification.timestamp) }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 任务报告对话框 -->
|
||||
<MissionReportDialog v-model:open="showMissionDialog" :report="selectedMissionReport" />
|
||||
|
||||
<!-- 被侦查星球 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.targetPlanet') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md flex items-center gap-2">
|
||||
<Globe class="h-4 w-4 text-blue-500" />
|
||||
<span class="font-medium">{{ selectedSpiedNotification.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">
|
||||
<div v-if="selectedSpiedNotification.detectionSuccess" class="flex items-center gap-2 text-yellow-600 dark:text-yellow-400">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
<span class="font-medium">{{ t('messagesView.detectionSuccess') }}</span>
|
||||
</div>
|
||||
<p class="text-sm mt-2">
|
||||
{{
|
||||
t('messagesView.spiedNotificationMessage', {
|
||||
npc: selectedSpiedNotification.npcName,
|
||||
planet: selectedSpiedNotification.targetPlanetName
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 建议 -->
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
{{ t('messagesView.spiedNotificationTip') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showSpiedDialog = false">{{ t('common.close') }}</Button>
|
||||
<Button @click="viewNPCInGalaxy(selectedSpiedNotification?.npcId)">{{ t('messagesView.viewInGalaxy') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 任务报告详情对话框 -->
|
||||
<Dialog :open="showMissionDialog" @update:open="showMissionDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
|
||||
{{ t('messagesView.missionReportDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.missionDetails') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedMissionReport" class="space-y-4">
|
||||
<!-- 任务状态 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ getMissionTypeName(selectedMissionReport.missionType) }}</h3>
|
||||
<Badge :variant="selectedMissionReport.success ? 'default' : 'destructive'">
|
||||
{{ selectedMissionReport.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedMissionReport.timestamp) }}
|
||||
</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">{{ selectedMissionReport.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="selectedMissionReport.targetPlanetName">{{ selectedMissionReport.targetPlanetName }}</p>
|
||||
<p class="text-sm text-muted-foreground" v-else>
|
||||
[{{ selectedMissionReport.targetPosition.galaxy }}:{{ selectedMissionReport.targetPosition.system }}:{{
|
||||
selectedMissionReport.targetPosition.position
|
||||
}}]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">{{ t('messagesView.missionDetails') }}</h4>
|
||||
<div class="p-3 bg-muted/30 rounded-md">
|
||||
<p class="text-sm mb-2">{{ selectedMissionReport.message }}</p>
|
||||
|
||||
<!-- 运输任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.transportedResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.transportedResources') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div v-for="res in basicResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回收任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.recycledResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.recycledResources') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMissionReport.details.remainingDebris" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.remainingDebris') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm text-yellow-600 dark:text-yellow-400">
|
||||
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 殖民任务详情 -->
|
||||
<div v-if="selectedMissionReport.details?.newPlanetName" class="mt-3">
|
||||
<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 text-green-500" />
|
||||
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导弹攻击详情 -->
|
||||
<div v-if="selectedMissionReport.details?.missileCount !== undefined" class="mt-3 space-y-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.missileAttack') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.missileCount') }}:</span>
|
||||
<span class="ml-1 font-medium">{{ selectedMissionReport.details.missileCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('missionReports.hits') }}:</span>
|
||||
<span class="ml-1 font-medium text-green-600">{{ selectedMissionReport.details.missileHits }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.intercepted') }}:</span>
|
||||
<span class="ml-1 font-medium text-yellow-600">{{ selectedMissionReport.details.missileIntercepted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Object.keys(selectedMissionReport.details.defenseLosses || {}).length > 0" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.defenseLosses') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-1 text-xs mt-1 p-2 bg-red-50 dark:bg-red-950/30 rounded">
|
||||
<div v-for="(count, defenseType) in selectedMissionReport.details.defenseLosses" :key="defenseType">
|
||||
<span class="text-muted-foreground">{{ t('defenses.' + defenseType) }}:</span>
|
||||
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 探险任务详情 - 发现资源 -->
|
||||
<div v-if="selectedMissionReport.details?.foundResources" class="mt-3 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-green-50 dark:bg-green-950/30 rounded">
|
||||
<div v-for="res in allResourceFields" :key="res.key">
|
||||
<template v-if="(selectedMissionReport.details?.foundResources?.[res.key] ?? 0) > 0">
|
||||
<span class="text-muted-foreground">{{ t(`resources.${res.key}`) }}:</span>
|
||||
<span class="ml-1 font-medium text-green-600 dark:text-green-400">
|
||||
+{{ (selectedMissionReport.details?.foundResources?.[res.key] ?? 0).toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 探险任务详情 - 发现舰船 -->
|
||||
<div v-if="selectedMissionReport.details?.foundFleet" class="mt-3 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-blue-50 dark:bg-blue-950/30 rounded">
|
||||
<div v-for="(count, shipType) in selectedMissionReport.details.foundFleet" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||
<span class="ml-1 font-medium text-blue-600 dark:text-blue-400">+{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 探险任务详情 - 损失舰船 -->
|
||||
<div v-if="selectedMissionReport.details?.fleetLost" class="mt-3 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-red-50 dark:bg-red-950/30 rounded">
|
||||
<div v-for="(count, shipType) in selectedMissionReport.details.fleetLost" :key="shipType">
|
||||
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showMissionDialog = false">{{ t('common.close') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- NPC活动通知详情对话框 -->
|
||||
<Dialog :open="showNPCActivityDialog" @update:open="showNPCActivityDialog = $event">
|
||||
<DialogContent class="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Recycle class="h-5 w-5 text-yellow-500" />
|
||||
{{ t('messagesView.npcActivityDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.activityDescription') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedNPCActivityNotification" class="space-y-4">
|
||||
<!-- NPC信息 -->
|
||||
<div class="p-4 bg-muted/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-lg">{{ selectedNPCActivityNotification.npcName }}</h3>
|
||||
<Badge variant="secondary">{{ t('messagesView.activityType.' + selectedNPCActivityNotification.activityType) }}</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ formatDate(selectedNPCActivityNotification.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 text-blue-500" />
|
||||
<span class="font-medium">
|
||||
{{ t('messagesView.position') }}: [{{ selectedNPCActivityNotification.targetPosition.galaxy }}:{{
|
||||
selectedNPCActivityNotification.targetPosition.system
|
||||
}}:{{ selectedNPCActivityNotification.targetPosition.position }}]
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="selectedNPCActivityNotification.targetPlanetName" class="text-sm text-muted-foreground">
|
||||
{{ t('messagesView.nearPlanet') }}: {{ selectedNPCActivityNotification.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: selectedNPCActivityNotification.npcName,
|
||||
activity: t('messagesView.activityType.' + selectedNPCActivityNotification.activityType),
|
||||
position: `[${selectedNPCActivityNotification.targetPosition.galaxy}:${selectedNPCActivityNotification.targetPosition.system}:${selectedNPCActivityNotification.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(selectedNPCActivityNotification.arrivalTime) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="p-3 bg-yellow-50 dark:bg-yellow-950/30 rounded-md border border-yellow-200 dark:border-yellow-800">
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
{{ t('messagesView.npcActivityTip') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showNPCActivityDialog = false">{{ t('common.close') }}</Button>
|
||||
<Button @click="viewLocationInGalaxy(selectedNPCActivityNotification?.targetPosition)">
|
||||
{{ t('messagesView.viewInGalaxy') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<!-- NPC活动通知对话框 -->
|
||||
<NPCActivityDialog v-model:open="showNPCActivityDialog" :notification="selectedNPCActivityNotification" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -662,19 +352,20 @@
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { FixedPagination } from '@/components/ui/pagination'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import BattleReportDialog from '@/components/dialogs/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/dialogs/SpyReportDialog.vue'
|
||||
import SpiedNotificationDialog from '@/components/dialogs/SpiedNotificationDialog.vue'
|
||||
import MissionReportDialog from '@/components/dialogs/MissionReportDialog.vue'
|
||||
import NPCActivityDialog from '@/components/dialogs/NPCActivityDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass, Trash2 } from 'lucide-vue-next'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Trash2 } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import type {
|
||||
BattleResult,
|
||||
@@ -690,7 +381,6 @@
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
@@ -739,13 +429,45 @@
|
||||
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' }]
|
||||
/**
|
||||
* 获取NPC当前名称
|
||||
* 优先使用当前NPC的实际名称,如果NPC不存在则使用通知中保存的旧名称
|
||||
* 支持通过ID查找,也支持通过旧名称中的ID模式匹配
|
||||
*/
|
||||
const getNpcName = (npcId: string | undefined, fallbackName: string): string => {
|
||||
if (!npcStore.npcs?.length) return fallbackName
|
||||
|
||||
// 全部资源字段配置(包含暗物质,用于探险任务)
|
||||
type AllResourceKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||
const allResourceFields: { key: AllResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
|
||||
// 1. 先通过 npcId 查找
|
||||
if (npcId) {
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
if (npc) return npc.name
|
||||
}
|
||||
|
||||
// 2. 尝试从旧名称中提取ID并查找
|
||||
// 旧格式如 "NPC-npc_182",新ID格式为 "npc_182"
|
||||
const idMatch = fallbackName.match(/npc_\d+/)
|
||||
if (idMatch) {
|
||||
const extractedId = idMatch[0]
|
||||
const npc = npcStore.npcs.find(n => n.id === extractedId)
|
||||
if (npc) return npc.name
|
||||
}
|
||||
|
||||
return fallbackName
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取侦查报告的目标名称
|
||||
* 显示 NPC 名称(如果是 NPC 星球)或星球名称
|
||||
*/
|
||||
const getSpyReportTargetName = (report: SpyReport): string => {
|
||||
// 尝试通过 targetPlayerId 获取 NPC 名称
|
||||
if (report.targetPlayerId && report.targetPlayerId !== 'unknown') {
|
||||
const npc = npcStore.npcs.find(n => n.id === report.targetPlayerId)
|
||||
if (npc) return npc.name
|
||||
}
|
||||
// 回退到星球名称
|
||||
return report.targetPlanetName || report.targetPlanetId
|
||||
}
|
||||
|
||||
const hasSelectedAny = computed(() => {
|
||||
return Object.values(clearOptions.value).some(v => v)
|
||||
@@ -1237,58 +959,4 @@
|
||||
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看NPC在星系中的位置
|
||||
const viewNPCInGalaxy = (npcId?: string) => {
|
||||
if (!npcId) return
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
if (!npc || npc.planets.length === 0) return
|
||||
|
||||
const targetPlanet = npc.planets[0]
|
||||
if (!targetPlanet) return
|
||||
|
||||
showSpiedDialog.value = false
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: targetPlanet.position.galaxy,
|
||||
system: targetPlanet.position.system,
|
||||
highlightNpc: npcId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看位置在星系中
|
||||
const viewLocationInGalaxy = (position?: { galaxy: number; system: number; position: number }) => {
|
||||
if (!position) return
|
||||
|
||||
showNPCActivityDialog.value = false
|
||||
router.push({
|
||||
path: '/galaxy',
|
||||
query: {
|
||||
galaxy: position.galaxy,
|
||||
system: position.system
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取任务类型图标
|
||||
const getMissionIcon = (missionType?: MissionType) => {
|
||||
if (!missionType) return Package
|
||||
|
||||
switch (missionType) {
|
||||
case MissionType.Transport:
|
||||
return Package
|
||||
case MissionType.Recycle:
|
||||
return Recycle
|
||||
case MissionType.Colonize:
|
||||
return Globe
|
||||
case MissionType.Expedition:
|
||||
return Compass
|
||||
case MissionType.Destroy:
|
||||
return Skull
|
||||
default:
|
||||
return Package
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||
{{ t('planet.position') }}: [{{ planet.position.galaxy }}:{{ planet.position.system }}:{{ planet.position.position }}]
|
||||
</p>
|
||||
<!-- 温度信息 -->
|
||||
<p v-if="planet.temperature && !planet.isMoon" class="text-xs sm:text-sm text-muted-foreground">
|
||||
{{ t('planet.temperature') }}: {{ planet.temperature.min }}°C {{ t('common.to') }} {{ planet.temperature.max }}°C
|
||||
</p>
|
||||
<!-- 月球信息 -->
|
||||
<div v-if="!planet.isMoon && moon" class="mt-2">
|
||||
<Button @click="switchToMoon" variant="outline" size="sm">
|
||||
@@ -28,11 +32,10 @@
|
||||
<CardContent>
|
||||
<Tabs default-value="overview" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">概览</TabsTrigger>
|
||||
<TabsTrigger value="production">产量详情</TabsTrigger>
|
||||
<TabsTrigger value="consumption">消耗详情</TabsTrigger>
|
||||
<TabsTrigger value="overview">{{ t('overview.tabOverview') }}</TabsTrigger>
|
||||
<TabsTrigger value="production">{{ t('overview.tabProduction') }}</TabsTrigger>
|
||||
<TabsTrigger value="consumption">{{ t('overview.tabConsumption') }}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 概览标签页 -->
|
||||
<TabsContent value="overview" class="mt-4">
|
||||
<Table>
|
||||
@@ -177,7 +180,7 @@
|
||||
<CardDescription>{{ t('overview.currentShips') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div class="grid grid-cols-3 sm:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div v-for="(count, shipType) in planet.fleet" :key="shipType">
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">{{ SHIPS[shipType].name }}</p>
|
||||
<p class="text-lg sm:text-xl font-bold">{{ count }}</p>
|
||||
@@ -198,7 +201,7 @@
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||
import ResourceIcon from '@/components/common/ResourceIcon.vue'
|
||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||
import { scaleNumber } from '@/utils/speed'
|
||||
import type { Planet } from '@/types/game'
|
||||
@@ -242,7 +245,23 @@
|
||||
]
|
||||
|
||||
// 消耗类型配置
|
||||
const consumptionTypes = [{ key: 'metalMine' as const }, { key: 'crystalMine' as const }, { key: 'deuteriumSynthesizer' as const }]
|
||||
const consumptionTypes = [
|
||||
// 资源建筑
|
||||
{ key: 'metalMine' as const },
|
||||
{ key: 'crystalMine' as const },
|
||||
{ key: 'deuteriumSynthesizer' as const },
|
||||
// 设施建筑
|
||||
{ key: 'roboticsFactory' as const },
|
||||
{ key: 'naniteFactory' as const },
|
||||
{ key: 'shipyard' as const },
|
||||
{ key: 'researchLab' as const },
|
||||
{ key: 'missileSilo' as const },
|
||||
{ key: 'terraformer' as const },
|
||||
{ key: 'darkMatterCollector' as const },
|
||||
// 月球建筑
|
||||
{ key: 'sensorPhalanx' as const },
|
||||
{ key: 'jumpGate' as const }
|
||||
]
|
||||
|
||||
// 月球相关
|
||||
const moon = computed(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user