feat: 新增Android平台支持及构建流程

集成Android平台相关目录与配置文件,包含Gradle构建脚本、资源文件、启动图标、Java入口、Proguard规则等,完善.gitignore以排除Android构建产物。更新CI流程,支持自动构建并发布Android APK。移除README中项目结构说明,简化文档。
This commit is contained in:
谦君
2025-12-20 00:48:36 +08:00
parent 20fb2bb6a4
commit 1368bb4445
97 changed files with 7859 additions and 335 deletions

View File

@@ -49,7 +49,47 @@ jobs:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ matrix.executable }}
# 2. 构建 Electron 客户端
# 2. 构建 Android APK (ARM64, ARMv7, x86_64)
build-android:
name: Build Android APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Build Frontend
run: |
pnpm install
pnpm run build
- name: Sync Capacitor
run: npx cap sync android
- name: Build APK (Release)
working-directory: android
run: ./gradlew assembleRelease
- name: Upload APK Artifacts
uses: actions/upload-artifact@v4
with:
name: android-apk
path: android/app/build/outputs/apk/release/*.apk
# 3. 构建 Electron 客户端
build-electron:
name: Build Electron (${{ matrix.os }})
runs-on: ${{ matrix.os }}
@@ -91,9 +131,9 @@ jobs:
pkg/*.dmg
pkg/*.AppImage
# 3. 发布 Release
# 4. 发布 Release
release:
needs: [ build-server, build-electron ]
needs: [ build-server, build-android, build-electron ]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -124,6 +164,9 @@ jobs:
# 移动 Electron 安装包 (排除 unpacked 目录)
find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \;
# 移动 Android APK
find ./raw-assets/android-apk -type f -name "*.apk" -exec cp {} ./final-release/ \; || true
# 检查结果
echo "Final assets to upload:"
ls -R ./final-release

9
.gitignore vendored
View File

@@ -28,3 +28,12 @@ docs
*.sw?
/docs
/docs/assets
# Android build outputs
android/.gradle
android/app/build
android/build
android/local.properties
android/.idea
android/*.iml
android/app/*.iml

View File

@@ -109,57 +109,6 @@ pnpm build
pnpm preview
```
## Project Structure
```
ogame-vue-ts/
├── public/ # Static assets
│ └── logo.svg # Application logo
├── src/
│ ├── assets/ # Dynamic assets
│ ├── components/ # Vue components
│ │ └── ui/ # shadcn-vue UI components
│ ├── composables/ # Vue composables
│ ├── config/ # Game configuration
│ ├── lib/ # Utility libraries
│ ├── locales/ # i18n translation files
│ ├── logic/ # Game logic modules
│ │ ├── buildingLogic.ts
│ │ ├── buildingValidation.ts
│ │ ├── fleetLogic.ts
│ │ ├── moonLogic.ts
│ │ ├── moonValidation.ts
│ │ ├── researchLogic.ts
│ │ ├── researchValidation.ts
│ │ ├── shipLogic.ts
│ │ └── shipValidation.ts
│ ├── router/ # Vue Router configuration
│ ├── stores/ # Pinia state stores
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ ├── views/ # Page components
│ │ ├── OverviewView.vue
│ │ ├── BuildingsView.vue
│ │ ├── ResearchView.vue
│ │ ├── ShipyardView.vue
│ │ ├── DefenseView.vue
│ │ ├── FleetView.vue
│ │ ├── GalaxyView.vue
│ │ ├── OfficersView.vue
│ │ ├── BattleSimulatorView.vue
│ │ ├── MessagesView.vue
│ │ └── SettingsView.vue
│ ├── App.vue # Root component
│ ├── main.ts # Application entry point
│ └── style.css # Global styles
├── .github/
│ └── ISSUE_TEMPLATE/ # GitHub issue templates
├── LICENSE # CC BY-NC 4.0 License
├── package.json # Project dependencies
├── tsconfig.json # TypeScript configuration
└── vite.config.ts # Vite configuration
```
## Available Languages
- 🇺🇸 English

View File

@@ -109,57 +109,6 @@ pnpm build
pnpm preview
```
## 项目结构
```
ogame-vue-ts/
├── public/ # 静态资源
│ └── logo.svg # 应用图标
├── src/
│ ├── assets/ # 动态资源
│ ├── components/ # Vue 组件
│ │ └── ui/ # shadcn-vue UI 组件
│ ├── composables/ # Vue 组合式函数
│ ├── config/ # 游戏配置
│ ├── lib/ # 工具库
│ ├── locales/ # 国际化翻译文件
│ ├── logic/ # 游戏逻辑模块
│ │ ├── buildingLogic.ts # 建筑逻辑
│ │ ├── buildingValidation.ts # 建筑验证
│ │ ├── fleetLogic.ts # 舰队逻辑
│ │ ├── moonLogic.ts # 月球逻辑
│ │ ├── moonValidation.ts # 月球验证
│ │ ├── researchLogic.ts # 研究逻辑
│ │ ├── researchValidation.ts # 研究验证
│ │ ├── shipLogic.ts # 舰船逻辑
│ │ └── shipValidation.ts # 舰船验证
│ ├── router/ # Vue Router 路由配置
│ ├── stores/ # Pinia 状态存储
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ │ ├── OverviewView.vue # 概览页面
│ │ ├── BuildingsView.vue # 建筑页面
│ │ ├── ResearchView.vue # 研究页面
│ │ ├── ShipyardView.vue # 船坞页面
│ │ ├── DefenseView.vue # 防御页面
│ │ ├── FleetView.vue # 舰队页面
│ │ ├── GalaxyView.vue # 银河页面
│ │ ├── OfficersView.vue # 军官页面
│ │ ├── BattleSimulatorView.vue # 战斗模拟器
│ │ ├── MessagesView.vue # 消息页面
│ │ └── SettingsView.vue # 设置页面
│ ├── App.vue # 根组件
│ ├── main.ts # 应用入口
│ └── style.css # 全局样式
├── .github/
│ └── ISSUE_TEMPLATE/ # GitHub issue 模板
├── LICENSE # CC BY-NC 4.0 许可证
├── package.json # 项目依赖
├── tsconfig.json # TypeScript 配置
└── vite.config.ts # Vite 配置
```
## 支持的语言
- 🇺🇸 English (英语)

101
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

82
android/app/build.gradle Normal file
View File

@@ -0,0 +1,82 @@
apply plugin: 'com.android.application'
android {
namespace = "games.wenzi.ogame"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "games.wenzi.ogame"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 14
versionName "1.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// 按 ABI 拆分 APK (arm64-v8a, armeabi-v7a, x86_64)
splits {
abi {
enable true
reset()
include "arm64-v8a", "armeabi-v7a", "x86_64"
universalApk false
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// 为每个 ABI 设置不同的 versionCode
applicationVariants.configureEach { variant ->
variant.outputs.configureEach { output ->
def abiVersionCode = [
"armeabi-v7a": 1,
"arm64-v8a": 2,
"x86_64": 3
]
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
output.versionCodeOverride = abiVersionCode[abi] * 1000 + defaultConfig.versionCode
output.outputFileName = "OGame-${defaultConfig.versionName}-${abi}.apk"
}
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,5 @@
package games.wenzi.ogame;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +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="package_name">games.wenzi.ogame</string>
<string name="custom_url_scheme">games.wenzi.ogame</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

29
android/build.gradle Normal file
View File

@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.0_@capacitor+core@8.0.0/node_modules/@capacitor/android/capacitor')

22
android/gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Normal file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
android/settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

16
android/variables.gradle Normal file
View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}

18
capacitor.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { CapacitorConfig } from '@capacitor/cli'
const config: CapacitorConfig = {
appId: 'games.wenzi.ogame',
appName: 'OGame',
webDir: 'docs',
server: {
androidScheme: 'https'
},
android: {
buildOptions: {
keystorePath: undefined,
keystoreAlias: undefined
}
}
}
export default config

View File

@@ -9,7 +9,7 @@
},
"private": true,
"version": "1.4.0",
"buildDate": "2025/12/19 12:29:46",
"buildDate": "2025/12/20 00:39:08",
"main": "dist-electron/main.js",
"type": "module",
"scripts": {
@@ -17,9 +17,14 @@
"build": "vue-tsc -b && vite build && node update-build-date.js",
"preview": "vite preview",
"build:server": "pnpm run build && go build",
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder"
"build:electron": "cross-env ELECTRON_BUILD=1 pnpm run build && electron-builder",
"build:android": "pnpm run build && npx cap sync android",
"build:apk": "pnpm run build:android && cd android && ./gradlew assembleRelease"
},
"dependencies": {
"@capacitor/android": "^8.0.0",
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^14.1.0",
@@ -50,6 +55,8 @@
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"postcss": "^8.5.6",
"postcss-preset-env": "^10.5.0",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",

1406
pnpm-lock.yaml generated
View File

@@ -11,6 +11,15 @@ importers:
.:
dependencies:
'@capacitor/android':
specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.0)
'@capacitor/cli':
specifier: ^8.0.0
version: 8.0.0
'@capacitor/core':
specifier: ^8.0.0
version: 8.0.0
'@tailwindcss/vite':
specifier: ^4.1.17
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
@@ -96,6 +105,12 @@ importers:
electron-vite:
specifier: ^5.0.0
version: 5.0.0(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
postcss:
specifier: ^8.5.6
version: 8.5.6
postcss-preset-env:
specifier: ^10.5.0
version: 10.5.0(postcss@8.5.6)
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
@@ -630,6 +645,307 @@ packages:
'@canvas/image-data@1.1.0':
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
'@capacitor/android@8.0.0':
resolution: {integrity: sha512-FrBSvVAC5JuLaYHNyDnwQny0/SYnP+xDQbc/KA4wInmRkMXLDv22fkx9aBJIDrxjuUVd+jsRih4SAt8FgMEzCw==}
peerDependencies:
'@capacitor/core': ^8.0.0
'@capacitor/cli@8.0.0':
resolution: {integrity: sha512-v9hEBi69xGxuuZhg55N031bMEenKaPSv71Il8C22VOOH6surDyv/MPeImN0oVfFc7eiklaW3rDFYVz6cmXfJWQ==}
engines: {node: '>=22.0.0'}
hasBin: true
'@capacitor/core@8.0.0':
resolution: {integrity: sha512-250HTVd/W/KdMygoqaedisvNbHbpbQTN2Hy/8ZYGm1nAqE0Fx7sGss4l0nDg33STxEdDhtVRoL2fIaaiukKseA==}
'@csstools/cascade-layer-name-parser@2.0.5':
resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
'@csstools/css-calc@2.1.4':
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-color-parser@3.1.0':
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-parser-algorithms@3.0.5':
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@csstools/media-query-list-parser@4.0.3':
resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==}
engines: {node: '>=18'}
peerDependencies:
'@csstools/css-parser-algorithms': ^3.0.5
'@csstools/css-tokenizer': ^3.0.4
'@csstools/postcss-alpha-function@1.0.1':
resolution: {integrity: sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-cascade-layers@5.0.2':
resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-function-display-p3-linear@1.0.1':
resolution: {integrity: sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-function@4.0.12':
resolution: {integrity: sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-mix-function@3.0.12':
resolution: {integrity: sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.2':
resolution: {integrity: sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-content-alt-text@2.0.8':
resolution: {integrity: sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-contrast-color-function@2.0.12':
resolution: {integrity: sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-exponential-functions@2.0.9':
resolution: {integrity: sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-font-format-keywords@4.0.0':
resolution: {integrity: sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-gamut-mapping@2.0.11':
resolution: {integrity: sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-gradients-interpolation-method@5.0.12':
resolution: {integrity: sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-hwb-function@4.0.12':
resolution: {integrity: sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-ic-unit@4.0.4':
resolution: {integrity: sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-initial@2.0.1':
resolution: {integrity: sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-is-pseudo-class@5.0.3':
resolution: {integrity: sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-light-dark-function@2.0.11':
resolution: {integrity: sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-logical-float-and-clear@3.0.0':
resolution: {integrity: sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-logical-overflow@2.0.0':
resolution: {integrity: sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-logical-overscroll-behavior@2.0.0':
resolution: {integrity: sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-logical-resize@3.0.0':
resolution: {integrity: sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-logical-viewport-units@3.0.4':
resolution: {integrity: sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-media-minmax@2.0.9':
resolution: {integrity: sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5':
resolution: {integrity: sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-nested-calc@4.0.0':
resolution: {integrity: sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-normalize-display-values@4.0.0':
resolution: {integrity: sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-oklab-function@4.0.12':
resolution: {integrity: sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-position-area-property@1.0.0':
resolution: {integrity: sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-progressive-custom-properties@4.2.1':
resolution: {integrity: sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-random-function@2.0.1':
resolution: {integrity: sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-relative-color-syntax@3.0.12':
resolution: {integrity: sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-scope-pseudo-class@4.0.1':
resolution: {integrity: sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-sign-functions@1.1.4':
resolution: {integrity: sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-stepped-value-functions@4.0.9':
resolution: {integrity: sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-system-ui-font-family@1.0.0':
resolution: {integrity: sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-text-decoration-shorthand@4.0.3':
resolution: {integrity: sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-trigonometric-functions@4.0.9':
resolution: {integrity: sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/postcss-unset-value@4.0.0':
resolution: {integrity: sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@csstools/selector-resolve-nested@3.1.0':
resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==}
engines: {node: '>=18'}
peerDependencies:
postcss-selector-parser: ^7.0.0
'@csstools/selector-specificity@5.0.0':
resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==}
engines: {node: '>=18'}
peerDependencies:
postcss-selector-parser: ^7.0.0
'@csstools/utilities@2.0.0':
resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
'@develar/schema-utils@2.6.5':
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
@@ -984,6 +1300,38 @@ packages:
'@internationalized/number@3.6.5':
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
'@ionic/cli-framework-output@2.2.8':
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
engines: {node: '>=16.0.0'}
'@ionic/utils-array@2.1.6':
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-fs@3.1.7':
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
engines: {node: '>=16.0.0'}
'@ionic/utils-object@2.1.6':
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
engines: {node: '>=16.0.0'}
'@ionic/utils-process@2.1.12':
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-stream@3.1.7':
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
engines: {node: '>=16.0.0'}
'@ionic/utils-subprocess@3.0.1':
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
engines: {node: '>=16.0.0'}
'@ionic/utils-terminal@2.3.5':
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
engines: {node: '>=16.0.0'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -1339,6 +1687,9 @@ packages:
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
'@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -1366,6 +1717,9 @@ packages:
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
'@types/slice-ansi@4.0.0':
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1590,6 +1944,13 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
autoprefixer@10.4.23:
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -1619,6 +1980,10 @@ packages:
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
birpc@2.9.0:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
@@ -1629,6 +1994,10 @@ packages:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
bplist-parser@0.3.2:
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
engines: {node: '>= 5.10.0'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -1757,6 +2126,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -1821,6 +2194,32 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
css-blank-pseudo@7.0.1:
resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
css-has-pseudo@7.0.3:
resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
css-prefers-color-scheme@10.0.0:
resolution: {integrity: sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
cssdb@8.5.2:
resolution: {integrity: sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1872,6 +2271,10 @@ packages:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
@@ -1963,6 +2366,10 @@ packages:
engines: {node: '>= 12.20.55'}
hasBin: true
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2106,6 +2513,9 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
@@ -2206,6 +2616,10 @@ packages:
engines: {node: 20 || >=22}
hasBin: true
glob@13.0.0:
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
engines: {node: 20 || >=22}
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
@@ -2337,6 +2751,10 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@4.1.3:
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -2384,6 +2802,11 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
is-finalizationregistry@1.1.1:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'}
@@ -2474,6 +2897,10 @@ packages:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2549,6 +2976,14 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
lazy-val@1.0.5:
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
@@ -2815,6 +3250,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
native-run@2.0.1:
resolution: {integrity: sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==}
engines: {node: '>=16.0.0'}
hasBin: true
negotiator@0.6.4:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'}
@@ -2867,6 +3307,10 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
@@ -2968,6 +3412,166 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-attribute-case-insensitive@7.0.1:
resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-clamp@4.1.0:
resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==}
engines: {node: '>=7.6.0'}
peerDependencies:
postcss: ^8.4.6
postcss-color-functional-notation@7.0.12:
resolution: {integrity: sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-color-hex-alpha@10.0.0:
resolution: {integrity: sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-color-rebeccapurple@10.0.0:
resolution: {integrity: sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-custom-media@11.0.6:
resolution: {integrity: sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-custom-properties@14.0.6:
resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-custom-selectors@8.0.5:
resolution: {integrity: sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-dir-pseudo-class@9.0.1:
resolution: {integrity: sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-double-position-gradients@6.0.4:
resolution: {integrity: sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-focus-visible@10.0.1:
resolution: {integrity: sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-focus-within@9.0.1:
resolution: {integrity: sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-font-variant@5.0.0:
resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==}
peerDependencies:
postcss: ^8.1.0
postcss-gap-properties@6.0.0:
resolution: {integrity: sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-image-set-function@7.0.0:
resolution: {integrity: sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-lab-function@7.0.12:
resolution: {integrity: sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-logical@8.1.0:
resolution: {integrity: sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-nesting@13.0.2:
resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-opacity-percentage@3.0.0:
resolution: {integrity: sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-overflow-shorthand@6.0.0:
resolution: {integrity: sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-page-break@3.0.4:
resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==}
peerDependencies:
postcss: ^8
postcss-place@10.0.0:
resolution: {integrity: sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-preset-env@10.5.0:
resolution: {integrity: sha512-xgxFQPAPxeWmsgy8cR7GM1PGAL/smA5E9qU7K//D4vucS01es3M0fDujhDJn3kY8Ip7/vVYcecbe1yY+vBo3qQ==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-pseudo-class-any-link@10.0.1:
resolution: {integrity: sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-replace-overflow-wrap@4.0.0:
resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==}
peerDependencies:
postcss: ^8.0.3
postcss-selector-not@8.0.1:
resolution: {integrity: sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==}
engines: {node: '>=18'}
peerDependencies:
postcss: ^8.4
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -3005,6 +3609,10 @@ packages:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3109,6 +3717,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@6.1.2:
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
@@ -3184,6 +3797,9 @@ packages:
sanitize-filename@1.6.3:
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
sax@1.1.4:
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
@@ -3278,10 +3894,17 @@ packages:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slice-ansi@3.0.0:
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
engines: {node: '>=8'}
slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -3321,6 +3944,10 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
@@ -3433,6 +4060,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
tiny-async-pool@1.3.0:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
@@ -3457,6 +4087,10 @@ packages:
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
@@ -3547,6 +4181,10 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
upath@1.2.0:
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
engines: {node: '>=4'}
@@ -3730,6 +4368,14 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
@@ -4427,6 +5073,334 @@ snapshots:
'@canvas/image-data@1.1.0':
optional: true
'@capacitor/android@8.0.0(@capacitor/core@8.0.0)':
dependencies:
'@capacitor/core': 8.0.0
'@capacitor/cli@8.0.0':
dependencies:
'@ionic/cli-framework-output': 2.2.8
'@ionic/utils-subprocess': 3.0.1
'@ionic/utils-terminal': 2.3.5
commander: 12.1.0
debug: 4.4.3
env-paths: 2.2.1
fs-extra: 11.3.2
kleur: 4.1.5
native-run: 2.0.1
open: 8.4.2
plist: 3.1.0
prompts: 2.4.2
rimraf: 6.1.2
semver: 7.7.3
tar: 6.2.1
tslib: 2.8.1
xml2js: 0.6.2
transitivePeerDependencies:
- supports-color
'@capacitor/core@8.0.0':
dependencies:
tslib: 2.8.1
'@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/color-helpers': 5.1.0
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/css-tokenizer@3.0.4': {}
'@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-alpha-function@1.0.1(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)':
dependencies:
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
postcss: 8.5.6
postcss-selector-parser: 7.1.1
'@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-color-function@4.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.6)':
dependencies:
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-hwb-function@4.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-ic-unit@4.0.4(postcss@8.5.6)':
dependencies:
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-initial@2.0.1(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.6)':
dependencies:
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
postcss: 8.5.6
postcss-selector-parser: 7.1.1
'@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/postcss-logical-resize@3.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.6)':
dependencies:
'@csstools/css-tokenizer': 3.0.4
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-media-minmax@2.0.9(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
postcss: 8.5.6
'@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
postcss: 8.5.6
'@csstools/postcss-nested-calc@4.0.0(postcss@8.5.6)':
dependencies:
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-normalize-display-values@4.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-oklab-function@4.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-position-area-property@1.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-random-function@2.0.1(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.6)':
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
'@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
'@csstools/postcss-sign-functions@1.1.4(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.6)':
dependencies:
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.6)':
dependencies:
'@csstools/color-helpers': 5.1.0
postcss: 8.5.6
postcss-value-parser: 4.2.0
'@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.6)':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
'@csstools/postcss-unset-value@4.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.1)':
dependencies:
postcss-selector-parser: 7.1.1
'@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)':
dependencies:
postcss-selector-parser: 7.1.1
'@csstools/utilities@2.0.0(postcss@8.5.6)':
dependencies:
postcss: 8.5.6
'@develar/schema-utils@2.6.5':
dependencies:
ajv: 6.12.6
@@ -4741,6 +5715,82 @@ snapshots:
dependencies:
'@swc/helpers': 0.5.17
'@ionic/cli-framework-output@2.2.8':
dependencies:
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-array@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-fs@3.1.7':
dependencies:
'@types/fs-extra': 8.1.5
debug: 4.4.3
fs-extra: 9.1.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-object@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-process@2.1.12':
dependencies:
'@ionic/utils-object': 2.1.6
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
signal-exit: 3.0.7
tree-kill: 1.2.2
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-stream@3.1.7':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-subprocess@3.0.1':
dependencies:
'@ionic/utils-array': 2.1.6
'@ionic/utils-fs': 3.1.7
'@ionic/utils-process': 2.1.12
'@ionic/utils-stream': 3.1.7
'@ionic/utils-terminal': 2.3.5
cross-spawn: 7.0.6
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-terminal@2.3.5':
dependencies:
'@types/slice-ansi': 4.0.0
debug: 4.4.3
signal-exit: 3.0.7
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
tslib: 2.8.1
untildify: 4.0.0
wrap-ansi: 7.0.0
transitivePeerDependencies:
- supports-color
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
@@ -5041,6 +6091,10 @@ snapshots:
'@types/file-saver@2.0.7': {}
'@types/fs-extra@8.1.5':
dependencies:
'@types/node': 24.10.2
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.10.2
@@ -5073,6 +6127,8 @@ snapshots:
dependencies:
'@types/node': 24.10.2
'@types/slice-ansi@4.0.0': {}
'@types/trusted-types@2.0.7': {}
'@types/verror@1.10.11':
@@ -5354,8 +6410,7 @@ snapshots:
assert-plus@1.0.0:
optional: true
astral-regex@2.0.0:
optional: true
astral-regex@2.0.0: {}
async-exit-hook@2.0.1: {}
@@ -5367,6 +6422,15 @@ snapshots:
at-least-node@1.0.0: {}
autoprefixer@10.4.23(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001760
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -5401,6 +6465,8 @@ snapshots:
baseline-browser-mapping@2.9.7: {}
big-integer@1.6.52: {}
birpc@2.9.0: {}
bl@4.1.0:
@@ -5412,6 +6478,10 @@ snapshots:
boolean@3.2.0:
optional: true
bplist-parser@0.3.2:
dependencies:
big-integer: 1.6.52
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -5591,6 +6661,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@12.1.0: {}
commander@2.20.3: {}
commander@5.1.0: {}
@@ -5647,6 +6719,26 @@ snapshots:
crypto-random-string@2.0.0: {}
css-blank-pseudo@7.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
css-has-pseudo@7.0.3(postcss@8.5.6):
dependencies:
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
css-prefers-color-scheme@10.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
cssdb@8.5.2: {}
cssesc@3.0.0: {}
csstype@3.2.3: {}
data-view-buffer@1.0.2:
@@ -5702,6 +6794,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
define-lazy-prop@2.0.0: {}
define-properties@1.2.1:
dependencies:
define-data-property: 1.1.4
@@ -5845,6 +6939,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
elementtree@0.1.7:
dependencies:
sax: 1.1.4
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -6059,6 +7157,8 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
fraction.js@5.3.4: {}
framer-motion@12.23.12:
dependencies:
motion-dom: 12.23.12
@@ -6174,6 +7274,12 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 2.0.1
glob@13.0.0:
dependencies:
minimatch: 10.1.1
minipass: 7.1.2
path-scurry: 2.0.1
glob@7.2.3:
dependencies:
fs.realpath: 1.0.0
@@ -6332,6 +7438,8 @@ snapshots:
inherits@2.0.4: {}
ini@4.1.3: {}
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -6387,6 +7495,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-docker@2.2.1: {}
is-finalizationregistry@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -6465,6 +7575,10 @@ snapshots:
is-what@5.5.0: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
isarray@2.0.5: {}
isbinaryfile@4.0.10: {}
@@ -6528,6 +7642,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kleur@3.0.3: {}
kleur@4.1.5: {}
lazy-val@1.0.5: {}
leven@3.1.0: {}
@@ -6758,6 +7876,22 @@ snapshots:
nanoid@3.3.11: {}
native-run@2.0.1:
dependencies:
'@ionic/utils-fs': 3.1.7
'@ionic/utils-terminal': 2.3.5
bplist-parser: 0.3.2
debug: 4.4.3
elementtree: 0.1.7
ini: 4.1.3
plist: 3.1.0
split2: 4.2.0
through2: 4.0.2
tslib: 2.8.1
yauzl: 2.10.0
transitivePeerDependencies:
- supports-color
negotiator@0.6.4: {}
node-abi@3.85.0:
@@ -6806,6 +7940,12 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
ora@5.4.1:
dependencies:
bl: 4.1.0
@@ -6889,6 +8029,231 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-clamp@4.1.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-color-functional-notation@7.0.12(postcss@8.5.6):
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-color-hex-alpha@10.0.0(postcss@8.5.6):
dependencies:
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-color-rebeccapurple@10.0.0(postcss@8.5.6):
dependencies:
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-custom-media@11.0.6(postcss@8.5.6):
dependencies:
'@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
postcss: 8.5.6
postcss-custom-properties@14.0.6(postcss@8.5.6):
dependencies:
'@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-custom-selectors@8.0.5(postcss@8.5.6):
dependencies:
'@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-dir-pseudo-class@9.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-double-position-gradients@6.0.4(postcss@8.5.6):
dependencies:
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-focus-visible@10.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-focus-within@9.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-font-variant@5.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-gap-properties@6.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-image-set-function@7.0.0(postcss@8.5.6):
dependencies:
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-lab-function@7.0.12(postcss@8.5.6):
dependencies:
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@csstools/css-tokenizer': 3.0.4
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/utilities': 2.0.0(postcss@8.5.6)
postcss: 8.5.6
postcss-logical@8.1.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-nesting@13.0.2(postcss@8.5.6):
dependencies:
'@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.1)
'@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1)
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-opacity-percentage@3.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-overflow-shorthand@6.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-page-break@3.0.4(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-place@10.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
postcss-preset-env@10.5.0(postcss@8.5.6):
dependencies:
'@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.6)
'@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6)
'@csstools/postcss-color-function': 4.0.12(postcss@8.5.6)
'@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.6)
'@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.6)
'@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.6)
'@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.6)
'@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.6)
'@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.6)
'@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.6)
'@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.6)
'@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.6)
'@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.6)
'@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.6)
'@csstools/postcss-initial': 2.0.1(postcss@8.5.6)
'@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.6)
'@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.6)
'@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.6)
'@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.6)
'@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.6)
'@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.6)
'@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.6)
'@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.6)
'@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.6)
'@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.6)
'@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.6)
'@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.6)
'@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.6)
'@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6)
'@csstools/postcss-random-function': 2.0.1(postcss@8.5.6)
'@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.6)
'@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.6)
'@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.6)
'@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.6)
'@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.6)
'@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.6)
'@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6)
'@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6)
autoprefixer: 10.4.23(postcss@8.5.6)
browserslist: 4.28.1
css-blank-pseudo: 7.0.1(postcss@8.5.6)
css-has-pseudo: 7.0.3(postcss@8.5.6)
css-prefers-color-scheme: 10.0.0(postcss@8.5.6)
cssdb: 8.5.2
postcss: 8.5.6
postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6)
postcss-clamp: 4.1.0(postcss@8.5.6)
postcss-color-functional-notation: 7.0.12(postcss@8.5.6)
postcss-color-hex-alpha: 10.0.0(postcss@8.5.6)
postcss-color-rebeccapurple: 10.0.0(postcss@8.5.6)
postcss-custom-media: 11.0.6(postcss@8.5.6)
postcss-custom-properties: 14.0.6(postcss@8.5.6)
postcss-custom-selectors: 8.0.5(postcss@8.5.6)
postcss-dir-pseudo-class: 9.0.1(postcss@8.5.6)
postcss-double-position-gradients: 6.0.4(postcss@8.5.6)
postcss-focus-visible: 10.0.1(postcss@8.5.6)
postcss-focus-within: 9.0.1(postcss@8.5.6)
postcss-font-variant: 5.0.0(postcss@8.5.6)
postcss-gap-properties: 6.0.0(postcss@8.5.6)
postcss-image-set-function: 7.0.0(postcss@8.5.6)
postcss-lab-function: 7.0.12(postcss@8.5.6)
postcss-logical: 8.1.0(postcss@8.5.6)
postcss-nesting: 13.0.2(postcss@8.5.6)
postcss-opacity-percentage: 3.0.0(postcss@8.5.6)
postcss-overflow-shorthand: 6.0.0(postcss@8.5.6)
postcss-page-break: 3.0.4(postcss@8.5.6)
postcss-place: 10.0.0(postcss@8.5.6)
postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.6)
postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6)
postcss-selector-not: 8.0.1(postcss@8.5.6)
postcss-pseudo-class-any-link@10.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-not@8.0.1(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 7.1.1
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -6915,6 +8280,11 @@ snapshots:
err-code: 2.0.3
retry: 0.12.0
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -7040,6 +8410,11 @@ snapshots:
dependencies:
glob: 7.2.3
rimraf@6.1.2:
dependencies:
glob: 13.0.0
package-json-from-dist: 1.0.1
roarr@2.15.4:
dependencies:
boolean: 3.2.0
@@ -7117,6 +8492,8 @@ snapshots:
dependencies:
truncate-utf8-bytes: 1.0.2
sax@1.1.4: {}
sax@1.4.3: {}
semver-compare@1.0.0:
@@ -7267,6 +8644,8 @@ snapshots:
dependencies:
semver: 7.7.3
sisteransi@1.0.5: {}
slice-ansi@3.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -7274,6 +8653,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0
optional: true
slice-ansi@4.0.0:
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
smart-buffer@4.2.0: {}
smob@1.5.0: {}
@@ -7308,6 +8693,8 @@ snapshots:
speakingurl@14.0.1: {}
split2@4.2.0: {}
sprintf-js@1.1.3:
optional: true
@@ -7452,6 +8839,10 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
tiny-async-pool@1.3.0:
dependencies:
semver: 5.7.2
@@ -7476,6 +8867,8 @@ snapshots:
dependencies:
punycode: 2.3.1
tree-kill@1.2.2: {}
truncate-utf8-bytes@1.0.2:
dependencies:
utf8-byte-length: 1.0.5
@@ -7577,6 +8970,8 @@ snapshots:
universalify@2.0.1: {}
untildify@4.0.0: {}
upath@1.2.0: {}
update-browserslist-db@1.2.2(browserslist@4.28.1):
@@ -7832,6 +9227,13 @@ snapshots:
wrappy@1.0.2: {}
xml2js@0.6.2:
dependencies:
sax: 1.4.3
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
xmlbuilder@15.1.1: {}
y18n@5.0.8: {}

15
postcss.config.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
plugins: {
'postcss-preset-env': {
stage: 2,
features: {
'oklab-function': { preserve: true },
'color-mix': true,
'custom-selectors': true,
'nesting-rules': true,
'is-pseudo-class': false
},
browsers: ['Chrome >= 80', 'Firefox >= 80', 'Safari >= 14', 'Edge >= 80']
}
}
}

View File

@@ -117,7 +117,8 @@
:data-nav-path="item.path"
:is-active="$route.path === item.path"
:tooltip="item.name.value"
@click="handleNavClick(item.path, $event)"
:disabled="!isFeatureUnlocked(item.path)"
@click="router.push(item.path)"
>
<component :is="item.icon" />
<span>{{ item.name.value }}</span>
@@ -259,7 +260,7 @@
</div>
</div>
<!-- 右侧展开按钮仅移动端 -->
<!-- 右侧队列通知 + 展开按钮 -->
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0 justify-end">
<!-- 移动端展开按钮 -->
<Button @click="resourceBarExpanded = !resourceBarExpanded" variant="ghost" size="sm" class="lg:hidden h-8 w-8 p-0">
@@ -330,6 +331,9 @@
<!-- 即将到来的敌对舰队警告 -->
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
<!-- 低电量警告 -->
<LowEnergyWarning />
<!-- 内容区域 -->
<main class="flex-1">
<Transition name="page" mode="out-in">
@@ -365,7 +369,6 @@
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
<!-- 返回顶部 -->
<BackToTop />
<!-- 队列通知 -->
<QueueNotifications />
@@ -443,6 +446,7 @@
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'
@@ -503,7 +507,8 @@
ChevronDown,
ChevronUp,
Handshake,
Pencil
Pencil,
Trophy
} from 'lucide-vue-next'
import * as gameLogic from '@/logic/gameLogic'
import * as planetLogic from '@/logic/planetLogic'
@@ -570,7 +575,8 @@
'/research': { building: BuildingType.ResearchLab, level: 1 },
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
'/defense': { building: BuildingType.Shipyard, level: 1 },
'/fleet': { building: BuildingType.Shipyard, level: 1 }
'/fleet': { building: BuildingType.Shipyard, level: 1 },
'/officers': { building: BuildingType.Shipyard, level: 1 }
}
// 判断是否为首页
@@ -599,6 +605,7 @@
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
{ 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.messages')), path: '/messages', icon: Mail },
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
// GM菜单在启用GM模式时显示
@@ -809,6 +816,9 @@
// NPC行为系统更新侦查和攻击决策
updateNPCBehavior(1)
// 检查成就解锁
checkAchievementUnlocks()
// 检查并处理被消灭的NPC所有星球都被摧毁的NPC
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
if (eliminatedNpcIds.length > 0) {
@@ -839,13 +849,28 @@
mission.targetPosition.position
)
// 先从玩家星球中查找,再从宇宙地图中查找
// 如果任务指定了targetIsMoon需要精确匹配行星或月球
const targetPlanet =
gameStore.player.planets.find(p => {
const positionMatch =
p.position.galaxy === mission.targetPosition.galaxy &&
p.position.system === mission.targetPosition.system &&
p.position.position === mission.targetPosition.position
// 如果任务明确指定目标类型,按类型匹配
if (mission.targetIsMoon !== undefined) {
return positionMatch && p.isMoon === mission.targetIsMoon
}
// 兼容旧任务:默认优先匹配行星(非月球)
return positionMatch && !p.isMoon
}) ||
// 如果没有匹配到指定类型,尝试匹配同位置的任何星球
gameStore.player.planets.find(
p =>
p.position.galaxy === mission.targetPosition.galaxy &&
p.position.system === mission.targetPosition.system &&
p.position.position === mission.targetPosition.position
) || universeStore.planets[targetKey]
) ||
universeStore.planets[targetKey]
// 获取起始星球名称(用于报告)
const originPlanet = gameStore.player.planets.find(p => p.id === mission.originPlanetId)
@@ -854,7 +879,32 @@
if (mission.missionType === MissionType.Transport) {
// 在处理任务之前保存货物信息因为processTransportArrival会清空cargo
const transportedResources = { ...mission.cargo }
const isGiftMission = mission.isGift && mission.giftTargetNpcId
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
// 更新成就统计(仅在成功时追踪)
if (result.success) {
const totalTransported =
transportedResources.metal + transportedResources.crystal + transportedResources.deuterium + transportedResources.darkMatter
if (isGiftMission) {
// 送礼成功
gameLogic.trackDiplomacyStats(gameStore.player, 'gift', { resourcesAmount: totalTransported })
} else {
// 普通运输任务成功
gameLogic.trackMissionStats(gameStore.player, 'transport', { resourcesAmount: totalTransported })
}
}
// 生成失败原因消息
let transportFailMessage = t('missionReports.transportFailed')
if (!result.success && result.failReason) {
if (result.failReason === 'targetNotFound') {
transportFailMessage = t('missionReports.transportFailedTargetNotFound')
} else if (result.failReason === 'giftRejected') {
transportFailMessage = t('missionReports.transportFailedGiftRejected')
}
}
// 生成运输任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -870,9 +920,10 @@
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: result.success,
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
message: result.success ? t('missionReports.transportSuccess') : transportFailMessage,
details: {
transportedResources
transportedResources,
failReason: result.failReason
},
read: false
})
@@ -881,6 +932,13 @@
if (attackResult) {
gameStore.player.battleReports.push(attackResult.battleResult)
// 更新成就统计 - 攻击
const debrisValue = attackResult.debrisField
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
: 0
const won = attackResult.battleResult.winner === 'attacker'
gameLogic.trackAttackStats(gameStore.player, attackResult.battleResult, won, debrisValue)
// 检查是否攻击了NPC星球更新外交关系
if (targetPlanet) {
const targetNpc = npcStore.npcs.find(npc => npc.planets.some(p => p.id === targetPlanet.id))
@@ -898,7 +956,24 @@
}
}
} else if (mission.missionType === MissionType.Colonize) {
const newPlanet = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
const colonizeResult = fleetLogic.processColonizeArrival(mission, targetPlanet, gameStore.player, t('planet.colonyPrefix'))
const newPlanet = colonizeResult.planet
// 更新成就统计 - 殖民
if (colonizeResult.success && newPlanet) {
gameLogic.trackMissionStats(gameStore.player, 'colonize')
}
// 生成失败原因消息
let failMessage = t('missionReports.colonizeFailed')
if (!colonizeResult.success && colonizeResult.failReason) {
if (colonizeResult.failReason === 'positionOccupied') {
failMessage = t('missionReports.colonizeFailedOccupied')
} else if (colonizeResult.failReason === 'maxColoniesReached') {
failMessage = t('missionReports.colonizeFailedMaxColonies')
}
}
// 生成殖民任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -912,24 +987,72 @@
targetPosition: mission.targetPosition,
targetPlanetId: newPlanet?.id,
targetPlanetName: newPlanet?.name,
success: !!newPlanet,
message: newPlanet ? t('missionReports.colonizeSuccess') : t('missionReports.colonizeFailed'),
success: colonizeResult.success,
message: colonizeResult.success ? t('missionReports.colonizeSuccess') : failMessage,
details: newPlanet
? {
newPlanetId: newPlanet.id,
newPlanetName: newPlanet.name
}
: undefined,
: { failReason: colonizeResult.failReason },
read: false
})
if (newPlanet) {
gameStore.player.planets.push(newPlanet)
}
} else if (mission.missionType === MissionType.Spy) {
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyReport) gameStore.player.spyReports.push(spyReport)
const spyResult = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
if (spyResult.success && spyResult.report) {
gameStore.player.spyReports.push(spyResult.report)
// 更新成就统计 - 侦查
gameLogic.trackMissionStats(gameStore.player, 'spy')
}
// 生成侦查任务报告(即使失败也生成)
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
}
let spyFailMessage = t('missionReports.spyFailed')
if (!spyResult.success && spyResult.failReason) {
if (spyResult.failReason === 'targetNotFound') {
spyFailMessage = t('missionReports.spyFailedTargetNotFound')
}
}
gameStore.player.missionReports.push({
id: `mission-report-${mission.id}`,
timestamp: Date.now(),
missionType: MissionType.Spy,
originPlanetId: mission.originPlanetId,
originPlanetName,
targetPosition: mission.targetPosition,
targetPlanetId: targetPlanet?.id,
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: spyResult.success,
message: spyResult.success ? t('missionReports.spySuccess') : spyFailMessage,
details: spyResult.success ? { spyReportId: spyResult.report?.id } : { failReason: spyResult.failReason },
read: false
})
} else if (mission.missionType === MissionType.Deploy) {
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies)
// 更新成就统计 - 部署
if (deployed.success) {
gameLogic.trackMissionStats(gameStore.player, 'deploy')
}
// 生成失败原因消息
let deployFailMessage = t('missionReports.deployFailed')
if (!deployed.success && deployed.failReason) {
if (deployed.failReason === 'targetNotFound') {
deployFailMessage = t('missionReports.deployFailedTargetNotFound')
} else if (deployed.failReason === 'notOwnPlanet') {
deployFailMessage = t('missionReports.deployFailedNotOwnPlanet')
}
}
// 生成部署任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -945,9 +1068,10 @@
targetPlanetName:
targetPlanet?.name || `[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`,
success: deployed.success,
message: deployed.success ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
message: deployed.success ? t('missionReports.deploySuccess') : deployFailMessage,
details: {
deployedFleet: mission.fleet
deployedFleet: mission.fleet,
failReason: deployed.failReason
},
read: false
})
@@ -962,6 +1086,23 @@
const debrisField = universeStore.debrisFields[debrisId]
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
// 更新成就统计 - 回收(无论是否有残骸都算飞行任务,但只有成功回收才计入回收资源量)
const totalRecycled =
recycleResult.success && recycleResult.collectedResources
? recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
: 0
gameLogic.trackMissionStats(gameStore.player, 'recycle', { resourcesAmount: totalRecycled })
// 生成失败原因消息
let recycleFailMessage = t('missionReports.recycleFailed')
if (!recycleResult.success && recycleResult.failReason) {
if (recycleResult.failReason === 'noDebrisField') {
recycleFailMessage = t('missionReports.recycleFailedNoDebris')
} else if (recycleResult.failReason === 'debrisEmpty') {
recycleFailMessage = t('missionReports.recycleFailedDebrisEmpty')
}
}
// 生成回收任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -973,18 +1114,18 @@
originPlanetId: mission.originPlanetId,
originPlanetName,
targetPosition: mission.targetPosition,
success: !!recycleResult,
message: recycleResult ? t('missionReports.recycleSuccess') : t('missionReports.recycleFailed'),
details: recycleResult
success: recycleResult.success,
message: recycleResult.success ? t('missionReports.recycleSuccess') : recycleFailMessage,
details: recycleResult.success
? {
recycledResources: recycleResult.collectedResources,
remainingDebris: recycleResult.remainingDebris || undefined
}
: undefined,
: { failReason: recycleResult.failReason },
read: false
})
if (recycleResult && debrisField) {
if (recycleResult.success && recycleResult.collectedResources && debrisField) {
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
// 更新残骸场
universeStore.debrisFields[debrisId] = {
@@ -1003,6 +1144,25 @@
// 处理行星毁灭任务
const destroyResult = fleetLogic.processDestroyArrival(mission, targetPlanet, gameStore.player)
// 更新成就统计 - 行星毁灭
if (destroyResult.success) {
gameLogic.trackMissionStats(gameStore.player, 'destroy')
}
// 生成失败原因消息
let destroyFailMessage = t('missionReports.destroyFailed')
if (!destroyResult.success && destroyResult.failReason) {
if (destroyResult.failReason === 'targetNotFound') {
destroyFailMessage = t('missionReports.destroyFailedTargetNotFound')
} else if (destroyResult.failReason === 'ownPlanet') {
destroyFailMessage = t('missionReports.destroyFailedOwnPlanet')
} else if (destroyResult.failReason === 'noDeathstar') {
destroyFailMessage = t('missionReports.destroyFailedNoDeathstar')
} else if (destroyResult.failReason === 'chanceFailed') {
destroyFailMessage = t('missionReports.destroyFailedChance', { chance: destroyResult.destructionChance.toFixed(1) })
}
}
// 生成毁灭任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -1016,19 +1176,23 @@
targetPosition: mission.targetPosition,
targetPlanetId: targetPlanet?.id,
targetPlanetName: targetPlanet?.name,
success: destroyResult?.success || false,
message: destroyResult?.success ? t('missionReports.destroySuccess') : t('missionReports.destroyFailed'),
details: destroyResult?.success
success: destroyResult.success,
message: destroyResult.success ? t('missionReports.destroySuccess') : destroyFailMessage,
details: destroyResult.success
? {
destroyedPlanetName:
targetPlanet?.name ||
`[${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}]`
}
: undefined,
: {
failReason: destroyResult.failReason,
destructionChance: destroyResult.destructionChance,
deathstarsLost: destroyResult.deathstarsLost
},
read: false
})
if (destroyResult && destroyResult.success && destroyResult.planetId) {
if (destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
// 处理外交关系如果目标是NPC星球
@@ -1036,6 +1200,20 @@
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
if (planetOwner) {
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
// 从NPC的星球列表中移除被摧毁的星球
const npcPlanetIndex = planetOwner.planets.findIndex(p => p.id === destroyResult.planetId)
if (npcPlanetIndex > -1) {
planetOwner.planets.splice(npcPlanetIndex, 1)
}
// 检查并处理被消灭的NPC所有星球都被摧毁的NPC
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
// 从npcStore中移除被消灭的NPC
if (eliminatedNpcIds.length > 0) {
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
}
}
}
@@ -1052,6 +1230,11 @@
// 处理远征任务
const expeditionResult = fleetLogic.processExpeditionArrival(mission)
// 更新成就统计 - 远征
const isSuccessful =
expeditionResult.eventType === 'resources' || expeditionResult.eventType === 'darkMatter' || expeditionResult.eventType === 'fleet'
gameLogic.trackMissionStats(gameStore.player, 'expedition', { successful: isSuccessful })
// 生成远征任务报告
if (!gameStore.player.missionReports) {
gameStore.player.missionReports = []
@@ -1129,7 +1312,13 @@
const debrisField = universeStore.debrisFields[debrisId]
const recycleResult = fleetLogic.processRecycleArrival(mission, debrisField)
if (recycleResult && debrisField) {
if (recycleResult && debrisField && recycleResult.collectedResources) {
// 更新成就统计 - 被NPC回收残骸如果残骸是玩家战斗产生的
const totalRecycled = recycleResult.collectedResources.metal + recycleResult.collectedResources.crystal
if (totalRecycled > 0) {
gameLogic.trackDiplomacyStats(gameStore.player, 'debrisRecycledByNPC', { resourcesAmount: totalRecycled })
}
if (recycleResult.remainingDebris && (recycleResult.remainingDebris.metal > 0 || recycleResult.remainingDebris.crystal > 0)) {
// 更新残骸场
universeStore.debrisFields[debrisId] = {
@@ -1175,6 +1364,9 @@
// NPC侦查到达
const { spiedNotification, spyReport } = npcBehaviorLogic.processNPCSpyArrival(npc, mission, targetPlanet, gameStore.player)
// 更新成就统计 - 被NPC侦查
gameLogic.trackDiplomacyStats(gameStore.player, 'spiedByNPC')
// 保存侦查报告到NPC用于后续攻击决策
if (!npc.playerSpyReports) {
npc.playerSpyReports = {}
@@ -1193,6 +1385,14 @@
// NPC攻击到达 - 使用专门的NPC攻击处理逻辑
fleetLogic.processNPCAttackArrival(npc, mission, targetPlanet, gameStore.player, gameStore.player.planets).then(attackResult => {
if (attackResult) {
// 更新成就统计 - 被NPC攻击 + 防御统计
gameLogic.trackDiplomacyStats(gameStore.player, 'attackedByNPC')
const debrisValue = attackResult.debrisField
? attackResult.debrisField.resources.metal + attackResult.debrisField.resources.crystal
: 0
const won = attackResult.battleResult.winner === 'defender'
gameLogic.trackDefenseStats(gameStore.player, attackResult.battleResult, won, debrisValue)
// 添加战斗报告给玩家
gameStore.player.battleReports.push(attackResult.battleResult)
@@ -1372,6 +1572,24 @@
}
}
/**
* 同步NPC星球数据到universeStore
* 解决npcStore和universeStore数据不同步的问题
*/
const syncNPCPlanetToUniverse = (npc: any) => {
npc.planets.forEach((npcPlanet: any) => {
const planetKey = gameLogic.generatePositionKey(npcPlanet.position.galaxy, npcPlanet.position.system, npcPlanet.position.position)
const universePlanet = universeStore.planets[planetKey]
if (universePlanet) {
// 同步所有关键数据
universePlanet.resources = { ...npcPlanet.resources }
universePlanet.buildings = { ...npcPlanet.buildings }
universePlanet.fleet = { ...npcPlanet.fleet }
universePlanet.defense = { ...npcPlanet.defense }
}
})
}
const updateNPCGrowth = (deltaSeconds: number) => {
// 累积时间
npcUpdateCounter.value += deltaSeconds
@@ -1432,20 +1650,20 @@
// 保存到store
npcStore.npcs = Array.from(npcMap.values())
// 如果有NPC基于玩家实力初始化NPC
// 如果有NPC基于距离初始化NPC实力
if (npcStore.npcs.length > 0) {
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
// 获取玩家母星(第一个非月球星球)
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
if (homeworld) {
npcStore.npcs.forEach(npc => {
// 基于距离初始化NPC实力
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
// 同步NPC星球数据到universeStore
syncNPCPlanetToUniverse(npc)
})
}
const playerPower = npcGrowthLogic.calculatePlayerAveragePower(gameState)
npcStore.npcs.forEach(npc => {
npcGrowthLogic.initializeNPCStartingPower(npc, playerPower)
})
// 初始化NPC之间的外交关系盟友/敌人)
npcGrowthLogic.initializeNPCDiplomacy(npcStore.npcs)
}
@@ -1459,6 +1677,9 @@
// 确保所有NPC都与玩家建立了关系修复旧版本保存的数据
if (npcStore.npcs.length > 0) {
const now = Date.now()
// 获取玩家母星(用于计算距离)
const homeworld = gameStore.player.planets.find(p => !p.isMoon)
npcStore.npcs.forEach(npc => {
if (!npc.relations) {
npc.relations = {}
@@ -1474,6 +1695,19 @@
history: []
}
}
// 迁移旧存档如果NPC没有距离数据计算并设置
if (homeworld && npc.distanceToHomeworld === undefined) {
const npcPlanet = npc.planets[0]
if (npcPlanet) {
npc.distanceToHomeworld = npcGrowthLogic.calculateDistanceToHomeworld(npcPlanet.position, homeworld.position)
npc.difficultyLevel = npcGrowthLogic.calculateDifficultyLevel(npc.distanceToHomeworld)
// 重新初始化NPC实力以匹配新的距离难度系统
npcGrowthLogic.initializeNPCByDistance(npc, homeworld.position)
// 同步NPC星球数据到universeStore
syncNPCPlanetToUniverse(npc)
}
}
})
}
@@ -1483,16 +1717,16 @@
return
}
// 构建游戏状态
const gameState: npcGrowthLogic.NPCGrowthGameState = {
planets: allPlanets,
player: gameStore.player,
npcs: npcStore.npcs
}
// 获取玩家母星用于距离计算
const homeworldForGrowth = gameStore.player.planets.find(p => !p.isMoon)
// 使用累积的时间更新每个NPC应用游戏速度倍率
// 使用累积的时间更新每个NPC基于距离的成长系统
npcStore.npcs.forEach(npc => {
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter.value, gameStore.gameSpeed)
if (homeworldForGrowth) {
npcGrowthLogic.updateNPCGrowthByDistance(npc, homeworldForGrowth.position, npcUpdateCounter.value, gameStore.gameSpeed)
// 同步NPC星球数据到universeStore确保侦查报告显示正确数据
syncNPCPlanetToUniverse(npc)
}
})
// 重置计数器
@@ -1566,6 +1800,56 @@
npcBehaviorCounter.value = 0
}
// 更新NPC关系统计友好/敌对数量)
const updateNPCRelationStats = () => {
let friendlyCount = 0
let hostileCount = 0
const playerId = gameStore.player.id
npcStore.npcs.forEach(npc => {
const relation = npc.relations?.[playerId]
if (relation) {
const status = diplomaticLogic.calculateRelationStatus(relation.reputation)
if (status === 'friendly') {
friendlyCount++
} else if (status === 'hostile') {
hostileCount++
}
}
})
gameLogic.trackDiplomacyStats(gameStore.player, 'updateRelations', { friendlyCount, hostileCount })
}
// 检查成就解锁
const achievementCheckCounter = ref(0)
const ACHIEVEMENT_CHECK_INTERVAL = 5 // 每5秒检查一次成就
const checkAchievementUnlocks = () => {
achievementCheckCounter.value += 1
// 只在达到更新间隔时才执行
if (achievementCheckCounter.value < ACHIEVEMENT_CHECK_INTERVAL) {
return
}
// 更新NPC关系统计
updateNPCRelationStats()
// 检查并解锁成就
const unlocks = gameLogic.checkAndUnlockAchievements(gameStore.player)
// 显示成就解锁通知(奖励已在 checkAndUnlockAchievements 中应用)
unlocks.forEach(unlock => {
// 显示 toast 通知
const tierName = t(`achievements.tiers.${unlock.tier}`)
const achievementName = t(`achievements.names.${unlock.id}`)
toast.success(t('achievements.unlocked'), {
description: `${achievementName} (${tierName})`
})
})
achievementCheckCounter.value = 0
}
// 启动游戏循环
const startGameLoop = () => {
if (gameStore.isPaused) return
@@ -1665,45 +1949,13 @@
}
// 检查功能是否解锁
const checkFeatureUnlocked = (path: string): { unlocked: boolean; requirement?: { building: BuildingType; level: number } } => {
const isFeatureUnlocked = (path: string): boolean => {
const requirement = featureRequirements[path]
if (!requirement) {
return { unlocked: true }
return true
}
const currentLevel = planet.value?.buildings[requirement.building] || 0
return {
unlocked: currentLevel >= requirement.level,
requirement
}
}
// 处理导航点击
const handleNavClick = (path: string, event: Event) => {
const { unlocked, requirement } = checkFeatureUnlocked(path)
if (!unlocked && requirement) {
event.preventDefault()
event.stopPropagation()
const buildingName = BUILDINGS.value[requirement.building]?.name || requirement.building
const currentLevel = planet.value?.buildings[requirement.building] || 0
toast.warning(t('common.featureLocked'), {
description: `${t('common.requiredBuilding')}: ${buildingName} Lv ${requirement.level} (${t(
'common.currentLevel'
)}: Lv ${currentLevel})`,
action: {
label: t('common.goToBuildings'),
onClick: () => router.push('/buildings')
},
duration: 3000
})
return
}
// 功能已解锁,正常导航
router.push(path)
return currentLevel >= requirement.level
}
// 切换到月球

View File

@@ -157,7 +157,7 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
import { Siren, Eye, Sword, Shield, Globe } from 'lucide-vue-next'
import { Siren, Eye, Sword, Shield, Globe, Recycle } from 'lucide-vue-next'
import { MissionType } from '@/types/game'
import type { IncomingFleetAlert } from '@/types/game'
import { formatDate, formatTime } from '@/utils/format'
@@ -200,6 +200,8 @@
return Eye
case MissionType.Attack:
return Sword
case MissionType.Recycle:
return Recycle
default:
return Siren
}
@@ -212,6 +214,8 @@
return 'text-purple-500'
case MissionType.Attack:
return 'text-red-500'
case MissionType.Recycle:
return 'text-amber-500'
default:
return 'text-yellow-500'
}
@@ -229,6 +233,8 @@
return t('enemyAlert.missionType.spy')
case MissionType.Attack:
return t('enemyAlert.missionType.attack')
case MissionType.Recycle:
return t('enemyAlert.missionType.recycle')
default:
return t('enemyAlert.missionType.unknown')
}
@@ -241,6 +247,8 @@
return t('enemyAlert.warning.spy')
case MissionType.Attack:
return t('enemyAlert.warning.attack')
case MissionType.Recycle:
return t('enemyAlert.warning.recycle')
default:
return t('enemyAlert.warning.unknown')
}

View File

@@ -0,0 +1,72 @@
<template>
<div v-if="showWarning" class="bg-destructive/10 border-b border-destructive/20">
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
<!-- 警告图标和信息 -->
<div class="flex items-center gap-2 flex-1 min-w-0">
<Zap class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-destructive">
{{ t('energy.lowWarning') }}
</p>
<p class="text-xs text-muted-foreground">
{{ detailMessage }}
</p>
</div>
</div>
<!-- 建造电站按钮 -->
<Button @click="goToBuildSolarPlant" variant="outline" size="sm" class="flex-shrink-0">
{{ t('energy.buildSolarPlant') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/gameStore'
import { Button } from '@/components/ui/button'
import { Zap } from 'lucide-vue-next'
import { useI18n } from '@/composables/useI18n'
import * as resourceLogic from '@/logic/resourceLogic'
import * as officerLogic from '@/logic/officerLogic'
const gameStore = useGameStore()
const router = useRouter()
const { t } = useI18n()
// 获取当前星球
const planet = computed(() => gameStore.currentPlanet)
// 计算能量产量
const energyProduction = computed(() => {
if (!planet.value) return 0
const now = Date.now()
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
return resourceLogic.calculateEnergyProduction(planet.value, { energyProductionBonus: bonuses.energyProductionBonus })
})
// 计算能量消耗
const energyConsumption = computed(() => {
if (!planet.value) return 0
return resourceLogic.calculateEnergyConsumption(planet.value)
})
// 是否显示警告(电力产量 < 消耗)
const showWarning = computed(() => {
if (!planet.value) return false
return energyProduction.value < energyConsumption.value
})
// 详细消息
const detailMessage = computed(() => {
const deficit = Math.ceil(energyConsumption.value - energyProduction.value)
return t('energy.deficitDetail', { deficit: deficit.toString() })
})
// 跳转到建筑页面建造太阳能电站
const goToBuildSolarPlant = () => {
router.push('/buildings')
}
</script>

View File

@@ -4,12 +4,20 @@
<CardHeader>
<div class="flex items-start justify-between">
<div class="flex-1">
<CardTitle class="flex items-center gap-2">
<CardTitle class="flex items-center gap-2 flex-wrap">
{{ npc.name }}
<span v-if="npc.note" class="text-muted-foreground font-normal">({{ npc.note }})</span>
<Badge :variant="statusBadgeVariant">
{{ statusText }}
</Badge>
<!-- NPC难度等级徽章 -->
<Badge
v-if="npc.difficultyLevel"
:variant="difficultyBadgeVariant"
:class="difficultyLevelColor"
>
Lv.{{ npc.difficultyLevel }}
</Badge>
</CardTitle>
<CardDescription class="mt-1">
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
@@ -208,6 +216,28 @@
return 'text-muted-foreground'
})
// NPC难度等级颜色
const difficultyLevelColor = computed(() => {
const level = props.npc.difficultyLevel
if (!level) return 'text-muted-foreground'
if (level <= 1) return 'text-green-600 dark:text-green-400' // 新手
if (level <= 2) return 'text-lime-600 dark:text-lime-400' // 简单
if (level <= 3) return 'text-yellow-600 dark:text-yellow-400' // 普通
if (level <= 4) return 'text-orange-600 dark:text-orange-400' // 困难
if (level <= 5) return 'text-red-600 dark:text-red-400' // 专家
if (level <= 6) return 'text-purple-600 dark:text-purple-400' // 大师
return 'text-pink-600 dark:text-pink-400' // 传奇及以上
})
// NPC难度等级Badge样式
const difficultyBadgeVariant = computed((): 'default' | 'secondary' | 'destructive' | 'outline' => {
const level = props.npc.difficultyLevel
if (!level) return 'outline'
if (level <= 2) return 'secondary'
if (level <= 4) return 'default'
return 'destructive'
})
// 最近的外交事件
const recentEvent = computed(() => {
if (!props.relation?.history || props.relation.history.length === 0) return null

View File

@@ -18,6 +18,15 @@
<div class="flex items-center gap-2">
<span class="font-medium truncate">{{ npc.name }}</span>
<span v-if="npc.note" class="text-muted-foreground text-sm truncate">({{ npc.note }})</span>
<!-- NPC难度等级徽章 -->
<Badge
v-if="npc.difficultyLevel"
:variant="difficultyBadgeVariant"
class="text-xs"
:class="difficultyLevelColor"
>
Lv.{{ npc.difficultyLevel }}
</Badge>
</div>
<div class="text-xs text-muted-foreground">
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
@@ -68,9 +77,18 @@
'bg-gray-400': status === RelationStatus.Neutral
}"
/>
<div class="flex-1 min-w-0">
<div class="flex-1 min-w-0 flex items-center gap-1 flex-wrap">
<span class="font-medium truncate">{{ npc.name }}</span>
<span v-if="npc.note" class="text-muted-foreground text-sm ml-1">({{ npc.note }})</span>
<span v-if="npc.note" class="text-muted-foreground text-sm">({{ npc.note }})</span>
<!-- NPC难度等级徽章 (移动端) -->
<Badge
v-if="npc.difficultyLevel"
:variant="difficultyBadgeVariant"
class="text-xs"
:class="difficultyLevelColor"
>
Lv.{{ npc.difficultyLevel }}
</Badge>
</div>
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" />
</div>
@@ -214,6 +232,28 @@
return 'text-muted-foreground'
})
// NPC难度等级颜色
const difficultyLevelColor = computed(() => {
const level = props.npc.difficultyLevel
if (!level) return 'text-muted-foreground'
if (level <= 1) return 'text-green-600 dark:text-green-400' // 新手
if (level <= 2) return 'text-lime-600 dark:text-lime-400' // 简单
if (level <= 3) return 'text-yellow-600 dark:text-yellow-400' // 普通
if (level <= 4) return 'text-orange-600 dark:text-orange-400' // 困难
if (level <= 5) return 'text-red-600 dark:text-red-400' // 专家
if (level <= 6) return 'text-purple-600 dark:text-purple-400' // 大师
return 'text-pink-600 dark:text-pink-400' // 传奇及以上
})
// NPC难度等级Badge样式
const difficultyBadgeVariant = computed((): 'default' | 'secondary' | 'destructive' | 'outline' => {
const level = props.npc.difficultyLevel
if (!level) return 'outline'
if (level <= 2) return 'secondary'
if (level <= 4) return 'default'
return 'destructive'
})
// 最近的外交事件
const recentEvent = computed(() => {
if (!props.relation?.history || props.relation.history.length === 0) return null

View File

@@ -173,7 +173,8 @@
const getQueueProgress = (item: BuildQueueItem): number => {
const elapsed = currentTime.value - item.startTime
const total = item.endTime - item.startTime
return Math.min(100, (elapsed / total) * 100)
if (total <= 0) return 100
return Math.max(0, Math.min(100, (elapsed / total) * 100))
}
// 统一的取消处理

View File

@@ -106,6 +106,14 @@ const hints: Hint[] = [
icon: 'swords',
delay: 500
},
{
id: 'achievements_intro',
route: '/achievements',
titleKey: 'hints.achievements.title',
messageKey: 'hints.achievements.message',
icon: 'trophy',
delay: 500
},
{
id: 'settings_intro',
route: '/settings',

View File

@@ -0,0 +1,555 @@
import { AchievementCategory, AchievementTier, type AchievementConfig } from '@/types/game'
// 成就配置
// 每个成就有5个等级青铜、白银、黄金、铂金、钻石
// 每个等级有对应的目标值和奖励(暗物质 + 积分)
export const ACHIEVEMENTS: AchievementConfig[] = [
// ==================== 资源类成就 ====================
{
id: 'metalCollector',
category: AchievementCategory.Resource,
icon: 'Gem',
statKey: 'totalMetalProduced',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'crystalCollector',
category: AchievementCategory.Resource,
icon: 'Diamond',
statKey: 'totalCrystalProduced',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 50000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 500000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 5000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 50000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'deuteriumCollector',
category: AchievementCategory.Resource,
icon: 'Droplet',
statKey: 'totalDeuteriumProduced',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 2500, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 25000, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 250000, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 2500000, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 25000000, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'darkMatterCollector',
category: AchievementCategory.Resource,
icon: 'Sparkles',
statKey: 'totalDarkMatterProduced',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 100, reward: { darkMatter: 20, points: 200 } },
{ tier: AchievementTier.Silver, target: 1000, reward: { darkMatter: 100, points: 1000 } },
{ tier: AchievementTier.Gold, target: 10000, reward: { darkMatter: 500, points: 5000 } },
{ tier: AchievementTier.Platinum, target: 100000, reward: { darkMatter: 2500, points: 25000 } },
{ tier: AchievementTier.Diamond, target: 1000000, reward: { darkMatter: 10000, points: 100000 } }
]
},
{
id: 'resourceConsumer',
category: AchievementCategory.Resource,
icon: 'Flame',
statKey: 'totalResourcesConsumed',
checkType: 'sum',
tiers: [
{ tier: AchievementTier.Bronze, target: 50000, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 500000, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 5000000, reward: { darkMatter: 350, points: 3500 } },
{ tier: AchievementTier.Platinum, target: 50000000, reward: { darkMatter: 1750, points: 17500 } },
{ tier: AchievementTier.Diamond, target: 500000000, reward: { darkMatter: 8500, points: 85000 } }
]
},
// ==================== 建造类成就 ====================
{
id: 'masterBuilder',
category: AchievementCategory.Building,
icon: 'Building2',
statKey: 'buildingsUpgraded',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 50, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 200, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 1000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'researcher',
category: AchievementCategory.Building,
icon: 'FlaskConical',
statKey: 'researchCompleted',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 250, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 500, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'shipwright',
category: AchievementCategory.Building,
icon: 'Rocket',
statKey: 'totalShipsProduced',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 500, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 2000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 10000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'fortifier',
category: AchievementCategory.Building,
icon: 'Shield',
statKey: 'totalDefensesBuilt',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 500, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 2000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 10000, reward: { darkMatter: 5000, points: 50000 } }
]
},
// ==================== 战斗类成就 ====================
{
id: 'warmonger',
category: AchievementCategory.Combat,
icon: 'Swords',
statKey: 'attacksLaunched',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 20, points: 200 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 100, points: 1000 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 400, points: 4000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 2000, points: 20000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 10000, points: 100000 } }
]
},
{
id: 'conqueror',
category: AchievementCategory.Combat,
icon: 'Crown',
statKey: 'attacksWon',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 25, points: 250 } },
{ tier: AchievementTier.Silver, target: 15, reward: { darkMatter: 125, points: 1250 } },
{ tier: AchievementTier.Gold, target: 50, reward: { darkMatter: 500, points: 5000 } },
{ tier: AchievementTier.Platinum, target: 200, reward: { darkMatter: 2500, points: 25000 } },
{ tier: AchievementTier.Diamond, target: 1000, reward: { darkMatter: 12500, points: 125000 } }
]
},
{
id: 'defender',
category: AchievementCategory.Combat,
icon: 'ShieldCheck',
statKey: 'defensesSuccessful',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 25, points: 250 } },
{ tier: AchievementTier.Silver, target: 15, reward: { darkMatter: 125, points: 1250 } },
{ tier: AchievementTier.Gold, target: 50, reward: { darkMatter: 500, points: 5000 } },
{ tier: AchievementTier.Platinum, target: 200, reward: { darkMatter: 2500, points: 25000 } },
{ tier: AchievementTier.Diamond, target: 1000, reward: { darkMatter: 12500, points: 125000 } }
]
},
{
id: 'fleetDestroyer',
category: AchievementCategory.Combat,
icon: 'Bomb',
statKey: 'totalEnemyFleetDestroyedInDefense',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 100, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 500, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 2000, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 10000, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'debrisCreator',
category: AchievementCategory.Combat,
icon: 'Trash2',
statKey: 'totalDebrisCreated',
checkType: 'sum',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'fleetSacrifice',
category: AchievementCategory.Combat,
icon: 'Skull',
statKey: 'totalFleetLost',
checkType: 'sum',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 500, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 2000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 10000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'defenseSacrifice',
category: AchievementCategory.Combat,
icon: 'ShieldOff',
statKey: 'totalDefenseLostInDefense',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 500, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 2000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 10000, reward: { darkMatter: 5000, points: 50000 } }
]
},
// ==================== 任务类成就 ====================
{
id: 'pilot',
category: AchievementCategory.Mission,
icon: 'Plane',
statKey: 'totalFlightMissions',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 50, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 200, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 1000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 5000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'transporter',
category: AchievementCategory.Mission,
icon: 'Truck',
statKey: 'transportMissions',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'cargoMaster',
category: AchievementCategory.Mission,
icon: 'Package',
statKey: 'transportedResources',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'colonizer',
category: AchievementCategory.Mission,
icon: 'Flag',
statKey: 'colonizations',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 1, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Silver, target: 3, reward: { darkMatter: 150, points: 1500 } },
{ tier: AchievementTier.Gold, target: 5, reward: { darkMatter: 500, points: 5000 } },
{ tier: AchievementTier.Platinum, target: 8, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 12, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'spy',
category: AchievementCategory.Mission,
icon: 'Eye',
statKey: 'spyMissions',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'deployer',
category: AchievementCategory.Mission,
icon: 'ArrowDownToLine',
statKey: 'deployments',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'explorer',
category: AchievementCategory.Mission,
icon: 'Compass',
statKey: 'expeditionsTotal',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'luckyExplorer',
category: AchievementCategory.Mission,
icon: 'Sparkle',
statKey: 'expeditionsSuccessful',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 20, points: 200 } },
{ tier: AchievementTier.Silver, target: 15, reward: { darkMatter: 100, points: 1000 } },
{ tier: AchievementTier.Gold, target: 50, reward: { darkMatter: 400, points: 4000 } },
{ tier: AchievementTier.Platinum, target: 200, reward: { darkMatter: 2000, points: 20000 } },
{ tier: AchievementTier.Diamond, target: 1000, reward: { darkMatter: 10000, points: 100000 } }
]
},
{
id: 'recycler',
category: AchievementCategory.Mission,
icon: 'Recycle',
statKey: 'recyclingMissions',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 25, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 100, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 500, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 2000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'scavenger',
category: AchievementCategory.Mission,
icon: 'Pickaxe',
statKey: 'recycledResources',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'destroyer',
category: AchievementCategory.Mission,
icon: 'Zap',
statKey: 'planetDestructions',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 1, reward: { darkMatter: 100, points: 1000 } },
{ tier: AchievementTier.Silver, target: 3, reward: { darkMatter: 500, points: 5000 } },
{ tier: AchievementTier.Gold, target: 10, reward: { darkMatter: 2000, points: 20000 } },
{ tier: AchievementTier.Platinum, target: 25, reward: { darkMatter: 10000, points: 100000 } },
{ tier: AchievementTier.Diamond, target: 50, reward: { darkMatter: 50000, points: 500000 } }
]
},
{
id: 'fuelBurner',
category: AchievementCategory.Mission,
icon: 'Fuel',
statKey: 'fuelConsumed',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 1000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 10000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 100000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 1000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 10000000, reward: { darkMatter: 5000, points: 50000 } }
]
},
// ==================== 外交类成就 ====================
{
id: 'diplomat',
category: AchievementCategory.Diplomacy,
icon: 'HandshakeIcon',
statKey: 'friendlyNPCCount',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 1, reward: { darkMatter: 20, points: 200 } },
{ tier: AchievementTier.Silver, target: 3, reward: { darkMatter: 100, points: 1000 } },
{ tier: AchievementTier.Gold, target: 5, reward: { darkMatter: 400, points: 4000 } },
{ tier: AchievementTier.Platinum, target: 10, reward: { darkMatter: 2000, points: 20000 } },
{ tier: AchievementTier.Diamond, target: 20, reward: { darkMatter: 10000, points: 100000 } }
]
},
{
id: 'nemesis',
category: AchievementCategory.Diplomacy,
icon: 'Angry',
statKey: 'hostileNPCCount',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 1, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 3, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 5, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 10, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 20, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'generous',
category: AchievementCategory.Diplomacy,
icon: 'Gift',
statKey: 'giftsSent',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 10, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 30, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 100, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 300, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'philanthropist',
category: AchievementCategory.Diplomacy,
icon: 'HeartHandshake',
statKey: 'giftResourcesTotal',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 15, points: 150 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 75, points: 750 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 300, points: 3000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1500, points: 15000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 7500, points: 75000 } }
]
},
{
id: 'target',
category: AchievementCategory.Diplomacy,
icon: 'Target',
statKey: 'attackedByNPC',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 10, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 30, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 100, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 300, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'watched',
category: AchievementCategory.Diplomacy,
icon: 'ScanEye',
statKey: 'spiedByNPC',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 5, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 20, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 50, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 150, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 500, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'robbed',
category: AchievementCategory.Diplomacy,
icon: 'Banknote',
statKey: 'debrisRecycledByNPC',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 3, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 10, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 30, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 100, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 300, reward: { darkMatter: 5000, points: 50000 } }
]
},
{
id: 'lostToNPC',
category: AchievementCategory.Diplomacy,
icon: 'BadgeDollarSign',
statKey: 'debrisResourcesLostToNPC',
checkType: 'gte',
tiers: [
{ tier: AchievementTier.Bronze, target: 10000, reward: { darkMatter: 10, points: 100 } },
{ tier: AchievementTier.Silver, target: 100000, reward: { darkMatter: 50, points: 500 } },
{ tier: AchievementTier.Gold, target: 1000000, reward: { darkMatter: 200, points: 2000 } },
{ tier: AchievementTier.Platinum, target: 10000000, reward: { darkMatter: 1000, points: 10000 } },
{ tier: AchievementTier.Diamond, target: 100000000, reward: { darkMatter: 5000, points: 50000 } }
]
}
]
// 成就ID映射方便快速查找
export const ACHIEVEMENT_MAP: Record<string, AchievementConfig> = Object.fromEntries(ACHIEVEMENTS.map(a => [a.id, a]))
// 按类别分组的成就
export const ACHIEVEMENTS_BY_CATEGORY: Record<AchievementCategory, AchievementConfig[]> = {
[AchievementCategory.Resource]: ACHIEVEMENTS.filter(a => a.category === AchievementCategory.Resource),
[AchievementCategory.Building]: ACHIEVEMENTS.filter(a => a.category === AchievementCategory.Building),
[AchievementCategory.Combat]: ACHIEVEMENTS.filter(a => a.category === AchievementCategory.Combat),
[AchievementCategory.Mission]: ACHIEVEMENTS.filter(a => a.category === AchievementCategory.Mission),
[AchievementCategory.Diplomacy]: ACHIEVEMENTS.filter(a => a.category === AchievementCategory.Diplomacy)
}
// 等级顺序(用于比较)
export const TIER_ORDER: AchievementTier[] = [
AchievementTier.Bronze,
AchievementTier.Silver,
AchievementTier.Gold,
AchievementTier.Platinum,
AchievementTier.Diamond
]
// 获取等级索引
export function getTierIndex(tier: AchievementTier): number {
return TIER_ORDER.indexOf(tier)
}
// 获取下一个等级
export function getNextTier(tier: AchievementTier | null): AchievementTier | null {
if (tier === null) return AchievementTier.Bronze
const index = getTierIndex(tier)
if (index >= TIER_ORDER.length - 1) return null
return TIER_ORDER[index + 1] ?? null
}

View File

@@ -59,6 +59,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5,
spaceUsage: 2,
planetOnly: true, // OGame规则月球不能建造太阳能电站
levelRequirements: {
15: { [BuildingType.RoboticsFactory]: 3 },
25: { [BuildingType.RoboticsFactory]: 6, [BuildingType.ResearchLab]: 5 },
@@ -73,6 +74,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 30,
costMultiplier: 1.8,
spaceUsage: 4,
planetOnly: true, // OGame规则月球不能建造核聚变反应堆
requirements: {
[TechnologyType.EnergyTechnology]: 3,
[BuildingType.DeuteriumSynthesizer]: 5
@@ -109,6 +111,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 2,
spaceUsage: 8,
maxLevel: 10, // 最多10级最多11个建造队列
planetOnly: true, // OGame规则月球不能建造纳米工厂
requirements: { [BuildingType.RoboticsFactory]: 10 },
levelRequirements: {
3: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 8, [TechnologyType.ComputerTechnology]: 8 },
@@ -139,6 +142,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 1.8,
spaceUsage: 3,
fleetStorageBonus: 1500, // 每级增加1500舰队仓储比船坞更高
planetOnly: true, // OGame规则月球不能建造机库
requirements: { [BuildingType.RoboticsFactory]: 1 }, // 只需要1级机器人工厂
levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 3 },
@@ -153,6 +157,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2,
spaceUsage: 3,
planetOnly: true, // OGame规则月球不能建造研究实验室
requirements: {
[BuildingType.MetalMine]: 3,
[BuildingType.CrystalMine]: 3,
@@ -176,11 +181,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 1,
requirements: { [BuildingType.MetalMine]: 2 },
levelRequirements: {
8: { [BuildingType.MetalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.MetalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
requirements: { [BuildingType.MetalMine]: 1 }
},
[BuildingType.CrystalStorage]: {
id: BuildingType.CrystalStorage,
@@ -190,11 +192,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 1,
requirements: { [BuildingType.CrystalMine]: 2 },
levelRequirements: {
8: { [BuildingType.CrystalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.CrystalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
requirements: { [BuildingType.CrystalMine]: 1 }
},
[BuildingType.DeuteriumTank]: {
id: BuildingType.DeuteriumTank,
@@ -204,11 +203,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2,
spaceUsage: 1,
requirements: { [BuildingType.DeuteriumSynthesizer]: 2 },
levelRequirements: {
8: { [BuildingType.DeuteriumSynthesizer]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.DeuteriumSynthesizer]: 25, [BuildingType.RoboticsFactory]: 6 }
}
planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
requirements: { [BuildingType.DeuteriumSynthesizer]: 1 }
},
[BuildingType.DarkMatterCollector]: {
id: BuildingType.DarkMatterCollector,
@@ -255,6 +251,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 2,
spaceUsage: 5,
maxLevel: 10,
planetOnly: true, // OGame规则月球不能建造导弹发射井
requirements: {
[BuildingType.Shipyard]: 1
},
@@ -1097,12 +1094,18 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
// 月球配置
export const MOON_CONFIG = {
minDebrisField: 100000, // 最小残骸场 (金属+晶体)
baseChance: 1, // 基础1%概率
maxChance: 20, // 最大20%概率
minDebrisField: 100000, // 最小残骸场 (金属+晶体) 100k = 1%概率
baseChance: 0, // 基础0%概率每100k残骸增加1%
maxChance: 20, // 最大20%概率需要2M残骸
chancePerDebris: 100000, // 每10万资源增加1%概率
baseSize: 100, // 月球基础空间
lunarBaseSpaceBonus: 30 // 每级月球基地增加的空间
baseFields: 1, // 月球初始空间OGame规则初始只有1格
lunarBaseFieldsBonus: 3, // 每级月球基地增加的空间(每级+3格占用1格净增2格
minDiameter: 3476, // 最小月球直径(km)1%概率时
maxDiameter: 8944, // 最大月球直径(km)20%概率时保证>8000km
baseDiameter: 3000, // 基础直径(km)
diameterPerChance: 273, // 每1%概率增加的直径(km)(8944-3476)/20≈273
jumpGateCooldown: 3600000 // 跳跃门冷却时间(ms) = 1小时 (OGame规则)
// 注月球资源容量与行星相同基础10000OGame规则允许资源超过容量存储
}
// 行星配置

View File

@@ -81,7 +81,8 @@ export default {
officers: 'Offiziere',
simulator: 'Simulator',
galaxy: 'Galaxie',
diplomacy: 'Diplomacy',
diplomacy: 'Diplomatie',
achievements: 'Erfolge',
messages: 'Nachrichten',
settings: 'Einstellungen',
gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: 'Stunde',
noEnergy: 'Keine Energie'
},
energy: {
lowWarning: 'Energiedefizit! Ressourcenproduktion gestoppt!',
severeWarning: 'Energiedefizit! Ressourcenproduktion gestoppt!',
criticalWarning: 'Energiedefizit! Ressourcenproduktion gestoppt!',
noProduction: 'Energiedefizit! Ressourcenproduktion gestoppt!',
deficitDetail: 'Energiedefizit: {deficit}, bauen Sie mehr Kraftwerke',
buildSolarPlant: 'Kraftwerk bauen'
},
planet: {
planet: 'Planet',
moon: 'Mond',
@@ -421,6 +430,7 @@ export default {
shipyardView: {
title: 'Raumschiffwerft',
fleetStorage: 'Flottenspeicher',
owned: 'Besitz',
attack: 'Angriff',
missileAttack: 'Raketenangriff',
shield: 'Schild',
@@ -486,8 +496,10 @@ export default {
available: 'Verfügbar',
all: 'Alle',
targetCoordinates: 'Zielkoordinaten',
targetType: 'Zieltyp',
planet: 'Planet',
moon: 'Mond',
galaxy: 'Galaxie',
diplomacy: 'Diplomacy',
system: 'System',
position: 'Position',
missionType: 'Missionstyp',
@@ -531,7 +543,30 @@ export default {
noDeathstar: 'Todesstern für Zerstörungsmission erforderlich',
giftMode: 'Geschenkmodus',
giftModeDescription: 'Ressourcen als Geschenk senden an',
estimatedReputationGain: 'Geschätzter Reputationsgewinn'
estimatedReputationGain: 'Geschätzter Reputationsgewinn',
// Flotten-Vorlagen
fleetPresets: 'Flotten-Vorlagen',
fleetPresetsDescription: 'Speichere häufige Flottenkonfigurationen für schnellen Versand (max. 3)',
savePreset: 'Vorlage speichern',
noPresets: 'Noch keine Vorlagen, wähle eine Flotte und klicke "Vorlage speichern"',
shipTypes: 'Schiffstypen',
editPreset: 'Vorlageninhalt bearbeiten',
renamePreset: 'Umbenennen',
deletePreset: 'Vorlage löschen',
editingPresetHint: 'Vorlage bearbeiten, Flottenkonfiguration ändern und "Speichern" klicken',
presetLimitReached: 'Vorlagenlimit erreicht',
presetLimitReachedMessage: 'Maximal {max} Vorlagen erlaubt',
presetError: 'Speichern fehlgeschlagen',
presetNoShips: 'Bitte wähle zuerst mindestens ein Schiff',
presetDefaultName: 'Vorlage {number}',
savePresetTitle: 'Flottenvorlage speichern',
savePresetDescription: 'Benenne diese Flottenkonfiguration',
renamePresetTitle: 'Vorlage umbenennen',
renamePresetDescription: 'Neuen Vorlagennamen eingeben',
presetName: 'Vorlagenname',
presetNamePlaceholder: 'Vorlagennamen eingeben',
deletePresetTitle: 'Vorlage löschen',
deletePresetMessage: 'Vorlage "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'
},
officersView: {
title: 'Offiziere',
@@ -571,7 +606,6 @@ export default {
title: 'Galaxie',
selectCoordinates: 'Koordinaten auswählen',
galaxy: 'Galaxie',
diplomacy: 'Diplomacy',
selectGalaxy: 'Galaxie auswählen',
system: 'System',
selectSystem: 'System auswählen',
@@ -733,14 +767,29 @@ export default {
missionReports: {
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
transportFailed: 'Transportmission fehlgeschlagen',
transportFailedTargetNotFound: 'Transport fehlgeschlagen: Zielplanet existiert nicht',
transportFailedGiftRejected: 'Transport fehlgeschlagen: Geschenk wurde abgelehnt',
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.',
spySuccess: 'Spionagemission erfolgreich abgeschlossen',
spyFailed: 'Spionagemission fehlgeschlagen',
spyFailedTargetNotFound: 'Spionage fehlgeschlagen: Zielplanet existiert nicht',
deploySuccess: 'Einsatzmission erfolgreich abgeschlossen',
deployFailed: 'Einsatzmission fehlgeschlagen',
deployFailedTargetNotFound: 'Einsatz fehlgeschlagen: Zielplanet existiert nicht',
deployFailedNotOwnPlanet: 'Einsatz fehlgeschlagen: Zielplanet gehört nicht dir',
recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen',
recycleFailed: 'Recyclingmission fehlgeschlagen, keine Trümmer am Zielort',
recycleFailedNoDebris: 'Recycling fehlgeschlagen: Kein Trümmerfeld am Zielort',
recycleFailedDebrisEmpty: 'Recycling fehlgeschlagen: Trümmerfeld ist bereits leer',
destroySuccess: 'Planetenzerstörungsmission erfolgreich ausgeführt',
destroyFailed: 'Planetenzerstörungsmission fehlgeschlagen',
destroyFailedTargetNotFound: 'Zerstörung fehlgeschlagen: Zielplanet existiert nicht',
destroyFailedOwnPlanet: 'Zerstörung fehlgeschlagen: Eigener Planet kann nicht zerstört werden',
destroyFailedNoDeathstar: 'Zerstörung fehlgeschlagen: Kein Todesstern für die Mission',
destroyFailedChance: 'Zerstörung fehlgeschlagen: Wahrscheinlichkeitsprüfung gescheitert (Erfolgsrate: {chance}%)',
missileAttackSuccess: 'Raketenangriff erfolgreich',
missileAttackFailed: 'Raketenangriff fehlgeschlagen, Zielplanet existiert nicht',
missileAttackIntercepted: 'Alle Raketen abgefangen',
@@ -1259,6 +1308,10 @@ export default {
message:
'Simulieren Sie Kampfergebnisse vor dem Angriff. Geben Sie Flotten und Technologiestufen ein, um Sieg, Verluste und Beute vorherzusagen.'
},
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.'
},
settings: {
title: 'Einstellungen',
message: 'Verwalten Sie hier Spieldaten, Benachrichtigungen und Import/Export. Sichern Sie regelmäßig Ihren Fortschritt!'
@@ -1268,5 +1321,102 @@ export default {
message:
'Der GM-Modus ermöglicht schnelle Änderung von Ressourcen, Gebäuden und Technologiestufen. Nutzen Sie ihn zum Testen oder für vollständige Spielinhalte.'
}
},
achievements: {
title: 'Erfolge',
unlocked: 'Erfolg freigeschaltet',
progress: 'Fortschritt',
nextTier: 'Nächste Stufe',
maxTierReached: 'Höchste Stufe erreicht',
tiers: {
bronze: 'Bronze',
silver: 'Silber',
gold: 'Gold',
platinum: 'Platin',
diamond: 'Diamant'
},
categories: {
resource: 'Ressourcen',
building: 'Gebäude',
combat: 'Kampf',
mission: 'Missionen',
diplomacy: 'Diplomatie'
},
names: {
metalCollector: 'Metallsammler',
crystalCollector: 'Kristallsammler',
deuteriumCollector: 'Deuteriumsammler',
darkMatterCollector: 'Dunkle-Materie-Sammler',
resourceConsumer: 'Ressourcenverbraucher',
masterBuilder: 'Meisterbauer',
researcher: 'Forscher',
shipwright: 'Schiffsbauer',
fortifier: 'Befestiger',
warmonger: 'Kriegstreiber',
conqueror: 'Eroberer',
defender: 'Verteidiger',
fleetDestroyer: 'Flottenzerstörer',
debrisCreator: 'Trümmererzeuger',
fleetSacrifice: 'Flottenopfer',
defenseSacrifice: 'Verteidigungsopfer',
pilot: 'Pilot',
transporter: 'Transporter',
cargoMaster: 'Frachtmeister',
colonizer: 'Kolonisator',
spy: 'Spionagemeister',
deployer: 'Stationierer',
explorer: 'Entdecker',
luckyExplorer: 'Glücklicher Entdecker',
recycler: 'Recycler',
scavenger: 'Sammler',
destroyer: 'Vernichter',
fuelBurner: 'Treibstoffverbrenner',
diplomat: 'Diplomat',
nemesis: 'Erzfeind',
generous: 'Großzügig',
philanthropist: 'Philanthrop',
target: 'Zielscheibe',
watched: 'Überwacht',
robbed: 'Beraubt',
lostToNPC: 'An NPC verloren'
},
descriptions: {
metalCollector: 'Gesamtes produziertes Metall',
crystalCollector: 'Gesamtes produziertes Kristall',
deuteriumCollector: 'Gesamtes produziertes Deuterium',
darkMatterCollector: 'Gesamte produzierte Dunkle Materie',
resourceConsumer: 'Gesamte verbrauchte Ressourcen',
masterBuilder: 'Gesamte Gebäudeupgrades',
researcher: 'Gesamte abgeschlossene Forschungen',
shipwright: 'Gesamte produzierte Schiffe',
fortifier: 'Gesamte gebaute Verteidigungen',
warmonger: 'Gestartete Angriffe',
conqueror: 'Gewonnene Angriffe',
defender: 'Erfolgreiche Verteidigungen',
fleetDestroyer: 'In der Verteidigung zerstörte feindliche Flotte',
debrisCreator: 'In Kämpfen erzeugte Trümmer',
fleetSacrifice: 'Gesamte verlorene Flotte',
defenseSacrifice: 'In der Verteidigung verlorene Verteidigungen',
pilot: 'Gesamte Flugmissionen',
transporter: 'Gesamte Transportmissionen',
cargoMaster: 'Gesamte transportierte Ressourcen',
colonizer: 'Kolonisierte Planeten',
spy: 'Abgeschlossene Spionagemissionen',
deployer: 'Abgeschlossene Stationierungen',
explorer: 'Gesamte Expeditionen',
luckyExplorer: 'Erfolgreiche Expeditionen',
recycler: 'Gesamte Recycling-Missionen',
scavenger: 'Gesamte recycelte Ressourcen',
destroyer: 'Zerstörte Planeten',
fuelBurner: 'Gesamter verbrauchter Treibstoff',
diplomat: 'Anzahl freundlicher NPCs',
nemesis: 'Anzahl feindlicher NPCs',
generous: 'Gesendete Geschenke',
philanthropist: 'Gesamte geschenkte Ressourcen',
target: 'Von NPC angegriffen',
watched: 'Von NPC ausspioniert',
robbed: 'Von NPC gesammelte Trümmer',
lostToNPC: 'An NPC verlorene Trümmerressourcen'
}
}
}

View File

@@ -81,6 +81,7 @@ export default {
simulator: 'Simulator',
galaxy: 'Galaxy',
diplomacy: 'Diplomacy',
achievements: 'Achievements',
messages: 'Messages',
settings: 'Settings',
gm: 'GM'
@@ -108,6 +109,14 @@ export default {
hour: 'hour',
noEnergy: 'No Energy'
},
energy: {
lowWarning: 'Energy deficit! Resource production stopped!',
severeWarning: 'Energy deficit! Resource production stopped!',
criticalWarning: 'Energy deficit! Resource production stopped!',
noProduction: 'Energy deficit! Resource production stopped!',
deficitDetail: 'Energy deficit: {deficit}, build more power plants',
buildSolarPlant: 'Build Power Plant'
},
planet: {
planet: 'Planet',
moon: 'Moon',
@@ -419,6 +428,7 @@ export default {
shipyardView: {
title: 'Shipyard',
fleetStorage: 'Fleet Storage',
owned: 'Owned',
attack: 'Attack',
missileAttack: 'Missile Attack',
shield: 'Shield',
@@ -482,6 +492,9 @@ export default {
available: 'Available',
all: 'All',
targetCoordinates: 'Target Coordinates',
targetType: 'Target Type',
planet: 'Planet',
moon: 'Moon',
galaxy: 'Galaxy',
system: 'System',
position: 'Position',
@@ -528,7 +541,47 @@ export default {
noDeathstar: 'Deathstar required for destruction mission',
giftMode: 'Gift Mode',
giftModeDescription: 'Send resources as a gift to',
estimatedReputationGain: 'Estimated reputation gain'
estimatedReputationGain: 'Estimated reputation gain',
// Fleet presets
fleetPresets: 'Fleet Presets',
fleetPresetsDescription: 'Save common fleet configurations for quick dispatch (max 3)',
savePreset: 'Save Preset',
noPresets: 'No presets yet, select fleet and click "Save Preset" to create',
shipTypes: 'ship types',
editPreset: 'Edit preset content',
renamePreset: 'Rename',
deletePreset: 'Delete preset',
editingPresetHint: 'Editing preset, modify fleet configuration and click "Save" to update',
presetLimitReached: 'Preset limit reached',
presetLimitReachedMessage: 'Maximum of {max} presets allowed',
presetError: 'Save failed',
presetNoShips: 'Please select at least one ship first',
presetDefaultName: 'Preset {number}',
savePresetTitle: 'Save Fleet Preset',
savePresetDescription: 'Name this fleet configuration',
renamePresetTitle: 'Rename Preset',
renamePresetDescription: 'Enter a new preset name',
presetName: 'Preset Name',
presetNamePlaceholder: 'Enter preset name',
deletePresetTitle: 'Delete Preset',
deletePresetMessage: 'Are you sure you want to delete preset "{name}"? This action cannot be undone.',
// Jump Gate
jumpGate: 'Jump Gate',
jumpGateDescription: 'Use the Jump Gate to instantly transfer fleet to another moon with Jump Gate',
jumpGateNotAvailable: 'Jump Gate Not Available',
jumpGateRequiresMoon: 'Jump Gate can only be used on moons',
jumpGateNotBuilt: 'Current moon does not have a Jump Gate',
jumpGateCooldown: 'Jump Gate Cooling Down',
jumpGateCooldownRemaining: 'Cooldown Remaining',
jumpGateReady: 'Jump Gate Ready',
jumpGateSelectTarget: 'Select Target Moon',
jumpGateNoTargetMoons: 'No available target moons (requires Jump Gate and cooldown complete)',
jumpGateSelectFleet: 'Select Fleet to Transfer',
jumpGateTransfer: 'Transfer Fleet',
jumpGateSuccess: 'Jump Gate Transfer Successful',
jumpGateSuccessMessage: 'Fleet has been instantly transferred to {target}',
jumpGateFailed: 'Jump Gate Transfer Failed',
jumpGateFailedMessage: 'Please check Jump Gate status and fleet configuration'
},
officersView: {
title: 'Officers',
@@ -616,7 +669,26 @@ export default {
giftPlanetTitle: 'Send Gift',
giftPlanetMessage:
'Are you sure you want to send resources as a gift to planet [{coordinates}]?\n\nPlease go to the fleet page to select transport ships and load resources.',
npcPlanetName: "{name}'s Planet"
npcPlanetName: "{name}'s Planet",
// Sensor Phalanx Scan
phalanxScan: 'Phalanx Scan',
phalanxScanTitle: 'Sensor Phalanx Scan',
phalanxScanDescription: 'Scanning fleet activity at planet [{coordinates}]',
phalanxNoMoon: 'Requires a moon with Sensor Phalanx to scan',
phalanxOutOfRange: 'Target is out of scan range',
phalanxRange: 'Scan Range',
phalanxCost: 'Scan Cost',
phalanxNoFleets: 'No fleet activity detected',
phalanxFleetDetected: '{count} fleet(s) detected',
phalanxMission: 'Mission',
phalanxOrigin: 'Origin',
phalanxDestination: 'Destination',
phalanxArrival: 'Arrival',
phalanxReturn: 'Return',
phalanxStatus: 'Status',
phalanxStatusOutbound: 'Outbound',
phalanxStatusReturning: 'Returning',
phalanxInsufficientDeuterium: 'Insufficient Deuterium'
},
messagesView: {
title: 'Messages',
@@ -726,14 +798,29 @@ export default {
missionReports: {
transportSuccess: 'Transport mission completed successfully',
transportFailed: 'Transport mission failed',
transportFailedTargetNotFound: 'Transport failed: Target planet does not exist',
transportFailedGiftRejected: 'Transport failed: Gift was rejected',
colonizeSuccess: 'Colonization mission successful, new planet established',
colonizeFailed: 'Colonization mission failed',
colonizeFailedOccupied: 'Colonization failed: Target position is already occupied by another planet',
colonizeFailedMaxColonies: 'Colonization failed: Maximum number of colonies reached. Research Astrophysics to increase the limit.',
spySuccess: 'Espionage mission completed successfully',
spyFailed: 'Espionage mission failed',
spyFailedTargetNotFound: 'Espionage failed: Target planet does not exist',
deploySuccess: 'Deployment mission completed successfully',
deployFailed: 'Deployment mission failed',
deployFailedTargetNotFound: 'Deployment failed: Target planet does not exist',
deployFailedNotOwnPlanet: 'Deployment failed: Target planet does not belong to you',
recycleSuccess: 'Recycling mission completed successfully',
recycleFailed: 'Recycling mission failed, no debris at target location',
recycleFailedNoDebris: 'Recycling failed: No debris field at target location',
recycleFailedDebrisEmpty: 'Recycling failed: Debris field has been cleared',
destroySuccess: 'Planet destruction mission executed successfully',
destroyFailed: 'Planet destruction mission failed',
destroyFailedTargetNotFound: 'Destruction failed: Target planet does not exist',
destroyFailedOwnPlanet: 'Destruction failed: Cannot destroy your own planet',
destroyFailedNoDeathstar: 'Destruction failed: No Death Star to execute the mission',
destroyFailedChance: 'Destruction failed: Probability check failed (Success rate: {chance}%)',
missileAttackSuccess: 'Missile attack successful',
missileAttackFailed: 'Missile attack failed, target planet does not exist',
missileAttackIntercepted: 'All missiles intercepted',
@@ -957,11 +1044,13 @@ export default {
missionType: {
spy: 'Spy',
attack: 'Attack',
recycle: 'Recycle',
unknown: 'Unknown'
},
warning: {
spy: 'Enemy spy incoming!',
attack: 'Enemy attack incoming!',
recycle: 'Enemy is recycling debris near you!',
unknown: 'Enemy fleet incoming!'
}
},
@@ -1376,6 +1465,10 @@ export default {
title: 'Battle Simulator',
message: 'Simulate battle outcomes before attacking. Enter both fleets and tech levels to predict victory, losses, and loot.'
},
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.'
},
settings: {
title: 'Settings',
message: 'Manage game data, adjust notifications, export/import saves here. Remember to backup your progress regularly!'
@@ -1385,5 +1478,102 @@ export default {
message:
'GM mode allows quick modification of resources, buildings, and tech levels. Use it for testing or experiencing full game content.'
}
},
achievements: {
title: 'Achievements',
unlocked: 'Achievement Unlocked',
progress: 'Progress',
nextTier: 'Next Tier',
maxTierReached: 'Max Tier Reached',
tiers: {
bronze: 'Bronze',
silver: 'Silver',
gold: 'Gold',
platinum: 'Platinum',
diamond: 'Diamond'
},
categories: {
resource: 'Resource',
building: 'Building',
combat: 'Combat',
mission: 'Mission',
diplomacy: 'Diplomacy'
},
names: {
metalCollector: 'Metal Collector',
crystalCollector: 'Crystal Collector',
deuteriumCollector: 'Deuterium Collector',
darkMatterCollector: 'Dark Matter Collector',
resourceConsumer: 'Resource Consumer',
masterBuilder: 'Master Builder',
researcher: 'Researcher',
shipwright: 'Shipwright',
fortifier: 'Fortifier',
warmonger: 'Warmonger',
conqueror: 'Conqueror',
defender: 'Defender',
fleetDestroyer: 'Fleet Destroyer',
debrisCreator: 'Debris Creator',
fleetSacrifice: 'Fleet Sacrifice',
defenseSacrifice: 'Defense Sacrifice',
pilot: 'Pilot',
transporter: 'Transporter',
cargoMaster: 'Cargo Master',
colonizer: 'Colonizer',
spy: 'Spy Master',
deployer: 'Deployer',
explorer: 'Explorer',
luckyExplorer: 'Lucky Explorer',
recycler: 'Recycler',
scavenger: 'Scavenger',
destroyer: 'Destroyer',
fuelBurner: 'Fuel Burner',
diplomat: 'Diplomat',
nemesis: 'Nemesis',
generous: 'Generous',
philanthropist: 'Philanthropist',
target: 'Target',
watched: 'Watched',
robbed: 'Robbed',
lostToNPC: 'Lost to NPC'
},
descriptions: {
metalCollector: 'Total metal produced',
crystalCollector: 'Total crystal produced',
deuteriumCollector: 'Total deuterium produced',
darkMatterCollector: 'Total dark matter produced',
resourceConsumer: 'Total resources consumed',
masterBuilder: 'Total buildings upgraded',
researcher: 'Total researches completed',
shipwright: 'Total ships produced',
fortifier: 'Total defenses built',
warmonger: 'Total attacks launched',
conqueror: 'Total attacks won',
defender: 'Total defenses successful',
fleetDestroyer: 'Enemy fleet destroyed in defense',
debrisCreator: 'Total debris created from battles',
fleetSacrifice: 'Total fleet lost',
defenseSacrifice: 'Total defenses lost in defense',
pilot: 'Total flight missions',
transporter: 'Total transport missions',
cargoMaster: 'Total resources transported',
colonizer: 'Planets colonized',
spy: 'Spy missions completed',
deployer: 'Deploy missions completed',
explorer: 'Total expeditions',
luckyExplorer: 'Successful expeditions',
recycler: 'Total recycling missions',
scavenger: 'Total resources recycled',
destroyer: 'Planets destroyed',
fuelBurner: 'Total fuel consumed',
diplomat: 'Number of friendly NPCs',
nemesis: 'Number of hostile NPCs',
generous: 'Total gifts sent',
philanthropist: 'Total resources gifted',
target: 'Times attacked by NPC',
watched: 'Times spied by NPC',
robbed: 'Times debris recycled by NPC',
lostToNPC: 'Total debris resources lost to NPC'
}
}
}

View File

@@ -81,9 +81,11 @@ export default {
officers: '士官',
simulator: 'シミュレーター',
galaxy: '銀河',
diplomacy: 'Diplomacy',
diplomacy: '外交',
achievements: '実績',
messages: 'メッセージ',
settings: '設定',
guide: 'ゲームガイド',
gm: 'GM'
},
sidebar: {
@@ -109,6 +111,14 @@ export default {
hour: '時間',
noEnergy: 'エネルギー不足'
},
energy: {
lowWarning: 'エネルギー不足!資源生産が停止しています!',
severeWarning: 'エネルギー不足!資源生産が停止しています!',
criticalWarning: 'エネルギー不足!資源生産が停止しています!',
noProduction: 'エネルギー不足!資源生産が停止しています!',
deficitDetail: 'エネルギー不足: {deficit}、発電所を建設してください',
buildSolarPlant: '発電所を建設'
},
planet: {
planet: '惑星',
moon: '月',
@@ -434,6 +444,7 @@ export default {
shipyardView: {
title: '造船所',
fleetStorage: '艦隊ストレージ',
owned: '所有',
attack: '攻撃力',
missileAttack: 'ミサイル攻撃',
shield: 'シールド',
@@ -484,8 +495,10 @@ export default {
available: '利用可能',
all: '全て',
targetCoordinates: '目標座標',
targetType: '目標タイプ',
planet: '惑星',
moon: '月',
galaxy: '銀河',
diplomacy: 'Diplomacy',
system: '星系',
position: '位置',
missionType: 'ミッションタイプ',
@@ -529,7 +542,30 @@ export default {
noDeathstar: '破壊ミッションにはデススターが必要です',
giftMode: 'ギフトモード',
giftModeDescription: '資源を贈り物として送る',
estimatedReputationGain: '推定評判獲得'
estimatedReputationGain: '推定評判獲得',
// 艦隊プリセット
fleetPresets: '艦隊プリセット',
fleetPresetsDescription: 'よく使う艦隊構成を保存して素早く派遣最大3つ',
savePreset: 'プリセット保存',
noPresets: 'プリセットなし、艦隊を選択して「プリセット保存」をクリック',
shipTypes: '種の艦船',
editPreset: 'プリセット内容を編集',
renamePreset: '名前変更',
deletePreset: 'プリセット削除',
editingPresetHint: 'プリセット編集中、艦隊構成を変更して「保存」をクリック',
presetLimitReached: 'プリセット上限到達',
presetLimitReachedMessage: '最大{max}個のプリセットまで',
presetError: '保存失敗',
presetNoShips: '艦船を1隻以上選択してください',
presetDefaultName: 'プリセット {number}',
savePresetTitle: '艦隊プリセットを保存',
savePresetDescription: 'この艦隊構成に名前を付ける',
renamePresetTitle: 'プリセット名変更',
renamePresetDescription: '新しいプリセット名を入力',
presetName: 'プリセット名',
presetNamePlaceholder: 'プリセット名を入力',
deletePresetTitle: 'プリセット削除',
deletePresetMessage: 'プリセット「{name}」を削除しますか?この操作は取り消せません。'
},
officersView: {
title: '士官',
@@ -569,7 +605,6 @@ export default {
title: '銀河',
selectCoordinates: '座標選択',
galaxy: '銀河',
diplomacy: 'Diplomacy',
selectGalaxy: '銀河を選択',
system: '星系',
selectSystem: '星系を選択',
@@ -726,14 +761,29 @@ export default {
missionReports: {
transportSuccess: '輸送ミッションが正常に完了しました',
transportFailed: '輸送ミッションが失敗しました',
transportFailedTargetNotFound: '輸送失敗:目標惑星が存在しません',
transportFailedGiftRejected: '輸送失敗:贈り物が拒否されました',
colonizeSuccess: '植民ミッション成功、新しい惑星が確立されました',
colonizeFailed: '植民ミッションが失敗しました',
colonizeFailedOccupied: '植民失敗:目標位置は既に他の惑星に占有されています',
colonizeFailedMaxColonies: '植民失敗:コロニー数の上限に達しました。天体物理学を研究して上限を増やしてください。',
spySuccess: 'スパイミッションが正常に完了しました',
spyFailed: 'スパイミッションが失敗しました',
spyFailedTargetNotFound: 'スパイ失敗:目標惑星が存在しません',
deploySuccess: '配備ミッションが正常に完了しました',
deployFailed: '配備ミッションが失敗しました',
deployFailedTargetNotFound: '配備失敗:目標惑星が存在しません',
deployFailedNotOwnPlanet: '配備失敗:目標惑星はあなたのものではありません',
recycleSuccess: '回収ミッションが正常に完了しました',
recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません',
recycleFailedNoDebris: '回収失敗:目標位置にデブリフィールドがありません',
recycleFailedDebrisEmpty: '回収失敗:デブリフィールドは既に空です',
destroySuccess: '惑星破壊ミッションが正常に実行されました',
destroyFailed: '惑星破壊ミッションが失敗しました',
destroyFailedTargetNotFound: '破壊失敗:目標惑星が存在しません',
destroyFailedOwnPlanet: '破壊失敗:自分の惑星を破壊することはできません',
destroyFailedNoDeathstar: '破壊失敗:ミッションを実行するデススターがありません',
destroyFailedChance: '破壊失敗:確率判定に失敗しました(成功率:{chance}%',
missileAttackSuccess: 'ミサイル攻撃成功',
missileAttackFailed: 'ミサイル攻撃失敗、目標惑星が存在しません',
missileAttackIntercepted: '全てのミサイルが迎撃されました',
@@ -946,11 +996,13 @@ export default {
missionType: {
spy: '偵察',
attack: '攻撃',
recycle: '回収',
unknown: '不明'
},
warning: {
spy: '敵の偵察が接近中!',
attack: '敵の攻撃が接近中!',
recycle: '敵があなたの近くでデブリを回収中!',
unknown: '敵艦隊が接近中!'
}
},
@@ -1238,6 +1290,10 @@ export default {
title: '戦闘シミュレーター',
message: '攻撃前に戦闘結果をシミュレート。双方の艦隊と技術レベルを入力して、勝敗と損失を予測。'
},
achievements: {
title: '実績システム',
message: 'ゲーム目標を達成して実績をアンロックし、ダークマター報酬を獲得!実績には複数のティアがあり、高い目標に挑戦してより良い報酬を手に入れましょう。'
},
settings: {
title: '設定',
message: 'ここでゲームデータの管理、通知設定、セーブのエクスポート/インポートができます。定期的にバックアップを!'
@@ -1246,5 +1302,102 @@ export default {
title: 'GM管理パネル',
message: 'GMモードでは資源、建物、技術レベルを素早く変更できます。テストや完全なゲームコンテンツの体験に使用。'
}
},
achievements: {
title: '実績',
unlocked: '実績解除',
progress: '進捗',
nextTier: '次のティア',
maxTierReached: '最高ティア達成',
tiers: {
bronze: 'ブロンズ',
silver: 'シルバー',
gold: 'ゴールド',
platinum: 'プラチナ',
diamond: 'ダイヤモンド'
},
categories: {
resource: '資源',
building: '建設',
combat: '戦闘',
mission: 'ミッション',
diplomacy: '外交'
},
names: {
metalCollector: 'メタルコレクター',
crystalCollector: 'クリスタルコレクター',
deuteriumCollector: 'デューテリウムコレクター',
darkMatterCollector: 'ダークマターコレクター',
resourceConsumer: '資源消費者',
masterBuilder: 'マスタービルダー',
researcher: '研究者',
shipwright: '造船士',
fortifier: '要塞化者',
warmonger: '戦争屋',
conqueror: '征服者',
defender: '防衛者',
fleetDestroyer: '艦隊破壊者',
debrisCreator: '残骸生成者',
fleetSacrifice: '艦隊犠牲者',
defenseSacrifice: '防衛犠牲者',
pilot: 'パイロット',
transporter: '輸送者',
cargoMaster: '貨物マスター',
colonizer: '植民者',
spy: 'スパイマスター',
deployer: '配備者',
explorer: '探検家',
luckyExplorer: '幸運な探検家',
recycler: 'リサイクラー',
scavenger: 'スカベンジャー',
destroyer: '破壊者',
fuelBurner: '燃料消費者',
diplomat: '外交官',
nemesis: '宿敵',
generous: '寛大な者',
philanthropist: '博愛者',
target: '標的',
watched: '監視下',
robbed: '略奪される',
lostToNPC: 'NPCに奪われた'
},
descriptions: {
metalCollector: '総メタル生産量',
crystalCollector: '総クリスタル生産量',
deuteriumCollector: '総デューテリウム生産量',
darkMatterCollector: '総ダークマター生産量',
resourceConsumer: '総資源消費量',
masterBuilder: '建物アップグレード総数',
researcher: '研究完了総数',
shipwright: '艦船生産総数',
fortifier: '防衛施設建設総数',
warmonger: '攻撃開始回数',
conqueror: '攻撃勝利回数',
defender: '防衛成功回数',
fleetDestroyer: '防衛で破壊した敵艦隊数',
debrisCreator: '戦闘で生成した残骸総量',
fleetSacrifice: '艦隊損失総数',
defenseSacrifice: '防衛で損失した防衛施設数',
pilot: '飛行ミッション総数',
transporter: '輸送ミッション総数',
cargoMaster: '輸送資源総量',
colonizer: '植民した惑星数',
spy: 'スパイミッション完了数',
deployer: '配備ミッション完了数',
explorer: '遠征総数',
luckyExplorer: '遠征成功回数',
recycler: 'リサイクルミッション総数',
scavenger: 'リサイクル資源総量',
destroyer: '破壊した惑星数',
fuelBurner: '燃料消費総量',
diplomat: '友好的なNPCの数',
nemesis: '敵対的なNPCの数',
generous: '贈り物送信回数',
philanthropist: '贈与資源総量',
target: 'NPCに攻撃された回数',
watched: 'NPCにスパイされた回数',
robbed: 'NPCに残骸を回収された回数',
lostToNPC: 'NPCに奪われた残骸資源総量'
}
}
}

View File

@@ -81,7 +81,8 @@ export default {
officers: '장교',
simulator: '시뮬레이터',
galaxy: '은하계',
diplomacy: 'Diplomacy',
diplomacy: '외교',
achievements: '업적',
messages: '메시지',
settings: '설정',
gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: '시간',
noEnergy: '에너지 부족'
},
energy: {
lowWarning: '에너지 부족! 자원 생산 중단!',
severeWarning: '에너지 부족! 자원 생산 중단!',
criticalWarning: '에너지 부족! 자원 생산 중단!',
noProduction: '에너지 부족! 자원 생산 중단!',
deficitDetail: '에너지 부족: {deficit}, 발전소를 더 건설하세요',
buildSolarPlant: '발전소 건설'
},
planet: {
planet: '행성',
moon: '위성',
@@ -420,6 +429,7 @@ export default {
shipyardView: {
title: '조선소',
fleetStorage: '함대 저장소',
owned: '보유',
attack: '공격력',
missileAttack: '미사일 공격',
shield: '실드',
@@ -484,8 +494,10 @@ export default {
available: '사용 가능',
all: '전체',
targetCoordinates: '목표 좌표',
targetType: '목표 유형',
planet: '행성',
moon: '달',
galaxy: '은하계',
diplomacy: 'Diplomacy',
system: '행성계',
position: '위치',
missionType: '임무 유형',
@@ -529,7 +541,30 @@ export default {
noDeathstar: '파괴 임무를 위해 데스스타가 필요합니다',
giftMode: '선물 모드',
giftModeDescription: '자원을 선물로 보내기',
estimatedReputationGain: '예상 평판 획득'
estimatedReputationGain: '예상 평판 획득',
// 함대 프리셋
fleetPresets: '함대 프리셋',
fleetPresetsDescription: '자주 사용하는 함대 구성을 저장하여 빠르게 파견 (최대 3개)',
savePreset: '프리셋 저장',
noPresets: '프리셋 없음, 함대를 선택하고 "프리셋 저장"을 클릭',
shipTypes: '종류의 함선',
editPreset: '프리셋 내용 편집',
renamePreset: '이름 변경',
deletePreset: '프리셋 삭제',
editingPresetHint: '프리셋 편집 중, 함대 구성을 변경하고 "저장"을 클릭',
presetLimitReached: '프리셋 한도 도달',
presetLimitReachedMessage: '최대 {max}개의 프리셋만 허용',
presetError: '저장 실패',
presetNoShips: '최소 하나의 함선을 선택하세요',
presetDefaultName: '프리셋 {number}',
savePresetTitle: '함대 프리셋 저장',
savePresetDescription: '이 함대 구성에 이름 지정',
renamePresetTitle: '프리셋 이름 변경',
renamePresetDescription: '새 프리셋 이름 입력',
presetName: '프리셋 이름',
presetNamePlaceholder: '프리셋 이름 입력',
deletePresetTitle: '프리셋 삭제',
deletePresetMessage: '프리셋 "{name}"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'
},
officersView: {
title: '장교',
@@ -569,7 +604,6 @@ export default {
title: '은하계',
selectCoordinates: '좌표 선택',
galaxy: '은하계',
diplomacy: 'Diplomacy',
selectGalaxy: '은하계 선택',
system: '행성계',
selectSystem: '행성계 선택',
@@ -728,14 +762,29 @@ export default {
missionReports: {
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
transportFailed: '수송 임무 실패',
transportFailedTargetNotFound: '수송 실패: 목표 행성이 존재하지 않습니다',
transportFailedGiftRejected: '수송 실패: 선물이 거절되었습니다',
colonizeSuccess: '식민 임무 성공, 새로운 행성이 건설되었습니다',
colonizeFailed: '식민 임무 실패',
colonizeFailedOccupied: '식민 실패: 목표 위치가 이미 다른 행성에 의해 점유되어 있습니다',
colonizeFailedMaxColonies: '식민 실패: 최대 식민지 수에 도달했습니다. 천체물리학을 연구하여 한도를 늘리세요.',
spySuccess: '정찰 임무가 성공적으로 완료되었습니다',
spyFailed: '정찰 임무 실패',
spyFailedTargetNotFound: '정찰 실패: 목표 행성이 존재하지 않습니다',
deploySuccess: '배치 임무가 성공적으로 완료되었습니다',
deployFailed: '배치 임무 실패',
deployFailedTargetNotFound: '배치 실패: 목표 행성이 존재하지 않습니다',
deployFailedNotOwnPlanet: '배치 실패: 목표 행성이 당신의 것이 아닙니다',
recycleSuccess: '회수 임무가 성공적으로 완료되었습니다',
recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다',
recycleFailedNoDebris: '회수 실패: 목표 위치에 잔해장이 없습니다',
recycleFailedDebrisEmpty: '회수 실패: 잔해장이 이미 비어 있습니다',
destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다',
destroyFailed: '행성 파괴 임무 실패',
destroyFailedTargetNotFound: '파괴 실패: 목표 행성이 존재하지 않습니다',
destroyFailedOwnPlanet: '파괴 실패: 자신의 행성을 파괴할 수 없습니다',
destroyFailedNoDeathstar: '파괴 실패: 임무를 수행할 데스스타가 없습니다',
destroyFailedChance: '파괴 실패: 확률 판정 실패 (성공률: {chance}%)',
missileAttackSuccess: '미사일 공격 성공',
missileAttackFailed: '미사일 공격 실패, 목표 행성이 존재하지 않습니다',
missileAttackIntercepted: '모든 미사일이 요격되었습니다',
@@ -948,11 +997,13 @@ export default {
missionType: {
spy: '정찰',
attack: '공격',
recycle: '회수',
unknown: '알 수 없음'
},
warning: {
spy: '적 정찰 접근 중!',
attack: '적 공격 접근 중!',
recycle: '적이 근처에서 잔해를 회수 중!',
unknown: '적 함대 접근 중!'
}
},
@@ -1240,6 +1291,10 @@ export default {
title: '전투 시뮬레이터',
message: '공격 전에 전투 결과를 시뮬레이션하세요. 양측 함대와 기술 레벨을 입력하여 승패와 손실을 예측.'
},
achievements: {
title: '업적 시스템',
message: '게임 목표를 완료하여 업적을 해제하고 암흑 물질 보상을 획득하세요! 업적은 여러 등급이 있으며, 더 높은 도전으로 더 좋은 보상을 받으세요.'
},
settings: {
title: '설정',
message: '여기서 게임 데이터 관리, 알림 설정, 저장 내보내기/가져오기가 가능합니다. 정기적으로 백업하세요!'
@@ -1248,5 +1303,102 @@ export default {
title: 'GM 관리 패널',
message: 'GM 모드에서는 자원, 건물, 기술 레벨을 빠르게 수정할 수 있습니다. 테스트나 전체 게임 콘텐츠 체험에 사용.'
}
},
achievements: {
title: '업적',
unlocked: '업적 해제',
progress: '진행도',
nextTier: '다음 등급',
maxTierReached: '최고 등급 달성',
tiers: {
bronze: '브론즈',
silver: '실버',
gold: '골드',
platinum: '플래티넘',
diamond: '다이아몬드'
},
categories: {
resource: '자원',
building: '건설',
combat: '전투',
mission: '임무',
diplomacy: '외교'
},
names: {
metalCollector: '금속 수집가',
crystalCollector: '크리스탈 수집가',
deuteriumCollector: '중수소 수집가',
darkMatterCollector: '암흑 물질 수집가',
resourceConsumer: '자원 소비자',
masterBuilder: '마스터 빌더',
researcher: '연구원',
shipwright: '조선공',
fortifier: '요새화자',
warmonger: '전쟁광',
conqueror: '정복자',
defender: '방어자',
fleetDestroyer: '함대 파괴자',
debrisCreator: '잔해 생성자',
fleetSacrifice: '함대 희생자',
defenseSacrifice: '방어 희생자',
pilot: '파일럿',
transporter: '운송가',
cargoMaster: '화물 마스터',
colonizer: '식민자',
spy: '스파이 마스터',
deployer: '배치자',
explorer: '탐험가',
luckyExplorer: '행운의 탐험가',
recycler: '재활용자',
scavenger: '스캐빈저',
destroyer: '파괴자',
fuelBurner: '연료 소비자',
diplomat: '외교관',
nemesis: '숙적',
generous: '관대한 자',
philanthropist: '자선가',
target: '표적',
watched: '감시 중',
robbed: '약탈당함',
lostToNPC: 'NPC에게 손실'
},
descriptions: {
metalCollector: '총 금속 생산량',
crystalCollector: '총 크리스탈 생산량',
deuteriumCollector: '총 중수소 생산량',
darkMatterCollector: '총 암흑 물질 생산량',
resourceConsumer: '총 자원 소비량',
masterBuilder: '건물 업그레이드 총 횟수',
researcher: '연구 완료 총 횟수',
shipwright: '함선 생산 총 수량',
fortifier: '방어 시설 건설 총 수량',
warmonger: '공격 발동 횟수',
conqueror: '공격 승리 횟수',
defender: '방어 성공 횟수',
fleetDestroyer: '방어에서 파괴한 적 함대 수',
debrisCreator: '전투에서 생성된 잔해 총량',
fleetSacrifice: '손실된 함대 총 수',
defenseSacrifice: '방어에서 손실된 방어 시설 수',
pilot: '비행 임무 총 횟수',
transporter: '운송 임무 총 횟수',
cargoMaster: '운송 자원 총량',
colonizer: '식민화한 행성 수',
spy: '스파이 임무 완료 횟수',
deployer: '배치 임무 완료 횟수',
explorer: '탐험 총 횟수',
luckyExplorer: '탐험 성공 횟수',
recycler: '재활용 임무 총 횟수',
scavenger: '재활용 자원 총량',
destroyer: '파괴한 행성 수',
fuelBurner: '연료 소비 총량',
diplomat: '우호적인 NPC 수',
nemesis: '적대적인 NPC 수',
generous: '선물 보낸 횟수',
philanthropist: '선물한 자원 총량',
target: 'NPC에게 공격당한 횟수',
watched: 'NPC에게 정찰당한 횟수',
robbed: 'NPC에게 잔해 회수당한 횟수',
lostToNPC: 'NPC에게 빼앗긴 잔해 자원 총량'
}
}
}

View File

@@ -81,7 +81,8 @@ export default {
officers: 'Офицеры',
simulator: 'Симулятор',
galaxy: 'Галактика',
diplomacy: 'Diplomacy',
diplomacy: 'Дипломатия',
achievements: 'Достижения',
messages: 'Сообщения',
settings: 'Настройки',
gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: 'час',
noEnergy: 'Нет энергии'
},
energy: {
lowWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
severeWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
criticalWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
noProduction: 'Дефицит энергии! Производство ресурсов остановлено!',
deficitDetail: 'Дефицит энергии: {deficit}, постройте больше электростанций',
buildSolarPlant: 'Построить электростанцию'
},
planet: {
planet: 'Планета',
moon: 'Луна',
@@ -422,6 +431,7 @@ export default {
shipyardView: {
title: 'Верфь',
fleetStorage: 'Хранилище флота',
owned: 'В наличии',
attack: 'Атака',
missileAttack: 'Ракетная атака',
shield: 'Щит',
@@ -487,8 +497,10 @@ export default {
available: 'Доступно',
all: 'Все',
targetCoordinates: 'Целевые координаты',
targetType: 'Тип цели',
planet: 'Планета',
moon: 'Луна',
galaxy: 'Галактика',
diplomacy: 'Diplomacy',
system: 'Система',
position: 'Позиция',
missionType: 'Тип миссии',
@@ -532,7 +544,30 @@ export default {
noDeathstar: 'Для миссии разрушения требуется Звезда Смерти',
giftMode: 'Режим подарка',
giftModeDescription: 'Отправить ресурсы в подарок',
estimatedReputationGain: 'Ожидаемый прирост репутации'
estimatedReputationGain: 'Ожидаемый прирост репутации',
// Шаблоны флота
fleetPresets: 'Шаблоны флота',
fleetPresetsDescription: 'Сохраняйте часто используемые конфигурации флота (макс. 3)',
savePreset: 'Сохранить шаблон',
noPresets: 'Нет шаблонов, выберите флот и нажмите "Сохранить шаблон"',
shipTypes: 'типов кораблей',
editPreset: 'Редактировать содержимое шаблона',
renamePreset: 'Переименовать',
deletePreset: 'Удалить шаблон',
editingPresetHint: 'Редактирование шаблона, измените конфигурацию и нажмите "Сохранить"',
presetLimitReached: 'Достигнут лимит шаблонов',
presetLimitReachedMessage: 'Максимум {max} шаблонов',
presetError: 'Ошибка сохранения',
presetNoShips: 'Выберите хотя бы один корабль',
presetDefaultName: 'Шаблон {number}',
savePresetTitle: 'Сохранить шаблон флота',
savePresetDescription: 'Назовите эту конфигурацию флота',
renamePresetTitle: 'Переименовать шаблон',
renamePresetDescription: 'Введите новое имя шаблона',
presetName: 'Имя шаблона',
presetNamePlaceholder: 'Введите имя шаблона',
deletePresetTitle: 'Удалить шаблон',
deletePresetMessage: 'Вы уверены, что хотите удалить шаблон "{name}"? Это действие нельзя отменить.'
},
officersView: {
title: 'Офицеры',
@@ -572,7 +607,6 @@ export default {
title: 'Галактика',
selectCoordinates: 'Выбрать координаты',
galaxy: 'Галактика',
diplomacy: 'Diplomacy',
selectGalaxy: 'Выбрать галактику',
system: 'Система',
selectSystem: 'Выбрать систему',
@@ -734,14 +768,29 @@ export default {
missionReports: {
transportSuccess: 'Миссия транспортировки успешно завершена',
transportFailed: 'Миссия транспортировки провалена',
transportFailedTargetNotFound: 'Транспортировка провалена: Целевая планета не существует',
transportFailedGiftRejected: 'Транспортировка провалена: Подарок был отклонён',
colonizeSuccess: 'Миссия колонизации успешна, новая планета создана',
colonizeFailed: 'Миссия колонизации провалена',
colonizeFailedOccupied: 'Колонизация провалена: Целевая позиция уже занята другой планетой',
colonizeFailedMaxColonies: 'Колонизация провалена: Достигнуто максимальное количество колоний. Исследуйте Астрофизику для увеличения лимита.',
spySuccess: 'Миссия шпионажа успешно завершена',
spyFailed: 'Миссия шпионажа провалена',
spyFailedTargetNotFound: 'Шпионаж провален: Целевая планета не существует',
deploySuccess: 'Миссия размещения успешно завершена',
deployFailed: 'Миссия размещения провалена',
deployFailedTargetNotFound: 'Размещение провалено: Целевая планета не существует',
deployFailedNotOwnPlanet: 'Размещение провалено: Целевая планета не принадлежит вам',
recycleSuccess: 'Миссия переработки успешно завершена',
recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции',
recycleFailedNoDebris: 'Переработка провалена: Нет поля обломков в целевой позиции',
recycleFailedDebrisEmpty: 'Переработка провалена: Поле обломков уже пусто',
destroySuccess: 'Миссия уничтожения планеты успешно выполнена',
destroyFailed: 'Миссия уничтожения планеты провалена',
destroyFailedTargetNotFound: 'Уничтожение провалено: Целевая планета не существует',
destroyFailedOwnPlanet: 'Уничтожение провалено: Нельзя уничтожить собственную планету',
destroyFailedNoDeathstar: 'Уничтожение провалено: Нет Звезды Смерти для выполнения миссии',
destroyFailedChance: 'Уничтожение провалено: Проверка вероятности не удалась (Шанс успеха: {chance}%)',
expeditionResources: 'Экспедиция обнаружила ресурсы!',
expeditionDarkMatter: 'Экспедиция обнаружила тёмную материю!',
expeditionFleet: 'Экспедиция обнаружила заброшенные корабли!',
@@ -1256,6 +1305,10 @@ export default {
title: 'Симулятор боя',
message: 'Симулируйте результаты боя перед атакой. Введите флоты и уровни технологий для прогноза победы, потерь и добычи.'
},
achievements: {
title: 'Система достижений',
message: 'Выполняйте игровые цели для разблокировки достижений и получения наград в виде тёмной материи! Достижения имеют несколько уровней - стремитесь к более высоким целям для лучших наград.'
},
settings: {
title: 'Настройки',
message: 'Управляйте игровыми данными, уведомлениями, импортом/экспортом сохранений. Регулярно создавайте резервные копии!'
@@ -1265,5 +1318,102 @@ export default {
message:
'Режим ГМ позволяет быстро изменять ресурсы, здания и уровни технологий. Используйте для тестирования или полного доступа к контенту.'
}
},
achievements: {
title: 'Достижения',
unlocked: 'Достижение разблокировано',
progress: 'Прогресс',
nextTier: 'Следующий уровень',
maxTierReached: 'Достигнут максимальный уровень',
tiers: {
bronze: 'Бронза',
silver: 'Серебро',
gold: 'Золото',
platinum: 'Платина',
diamond: 'Алмаз'
},
categories: {
resource: 'Ресурсы',
building: 'Строительство',
combat: 'Бой',
mission: 'Миссии',
diplomacy: 'Дипломатия'
},
names: {
metalCollector: 'Коллекционер металла',
crystalCollector: 'Коллекционер кристаллов',
deuteriumCollector: 'Коллекционер дейтерия',
darkMatterCollector: 'Коллекционер тёмной материи',
resourceConsumer: 'Потребитель ресурсов',
masterBuilder: 'Мастер-строитель',
researcher: 'Исследователь',
shipwright: 'Кораблестроитель',
fortifier: 'Укрепитель',
warmonger: 'Поджигатель войны',
conqueror: 'Завоеватель',
defender: 'Защитник',
fleetDestroyer: 'Уничтожитель флота',
debrisCreator: 'Создатель обломков',
fleetSacrifice: 'Жертва флота',
defenseSacrifice: 'Жертва обороны',
pilot: 'Пилот',
transporter: 'Транспортёр',
cargoMaster: 'Мастер груза',
colonizer: 'Колонизатор',
spy: 'Мастер шпионажа',
deployer: 'Размещатель',
explorer: 'Исследователь',
luckyExplorer: 'Счастливый исследователь',
recycler: 'Переработчик',
scavenger: 'Мусорщик',
destroyer: 'Уничтожитель',
fuelBurner: 'Сжигатель топлива',
diplomat: 'Дипломат',
nemesis: 'Заклятый враг',
generous: 'Щедрый',
philanthropist: 'Филантроп',
target: 'Мишень',
watched: 'Под наблюдением',
robbed: 'Ограблен',
lostToNPC: 'Потеряно НПС'
},
descriptions: {
metalCollector: 'Всего добыто металла',
crystalCollector: 'Всего добыто кристаллов',
deuteriumCollector: 'Всего добыто дейтерия',
darkMatterCollector: 'Всего добыто тёмной материи',
resourceConsumer: 'Всего потреблено ресурсов',
masterBuilder: 'Всего улучшений зданий',
researcher: 'Всего завершённых исследований',
shipwright: 'Всего произведённых кораблей',
fortifier: 'Всего построенной обороны',
warmonger: 'Всего начатых атак',
conqueror: 'Всего выигранных атак',
defender: 'Всего успешных защит',
fleetDestroyer: 'Вражеский флот уничтожен в обороне',
debrisCreator: 'Всего обломков созданных в боях',
fleetSacrifice: 'Всего потерянного флота',
defenseSacrifice: 'Всего потерянной обороны',
pilot: 'Всего лётных миссий',
transporter: 'Всего транспортных миссий',
cargoMaster: 'Всего перевезённых ресурсов',
colonizer: 'Колонизировано планет',
spy: 'Завершённых шпионских миссий',
deployer: 'Завершённых миссий размещения',
explorer: 'Всего экспедиций',
luckyExplorer: 'Успешных экспедиций',
recycler: 'Всего миссий переработки',
scavenger: 'Всего переработанных ресурсов',
destroyer: 'Уничтожено планет',
fuelBurner: 'Всего израсходовано топлива',
diplomat: 'Количество дружественных НПС',
nemesis: 'Количество враждебных НПС',
generous: 'Всего отправленных подарков',
philanthropist: 'Всего подаренных ресурсов',
target: 'Раз атакован НПС',
watched: 'Раз шпионил НПС',
robbed: 'Раз НПС собрал обломки',
lostToNPC: 'Всего обломков потеряно НПС'
}
}
}

View File

@@ -81,6 +81,7 @@ export default {
simulator: '模拟',
galaxy: '星系',
diplomacy: '外交',
achievements: '成就',
messages: '消息',
settings: '设置',
gm: 'GM'
@@ -108,6 +109,14 @@ export default {
hour: '小时',
noEnergy: '电力不足'
},
energy: {
lowWarning: '电力不足,资源生产已停止!',
severeWarning: '电力不足,资源生产已停止!',
criticalWarning: '电力不足,资源生产已停止!',
noProduction: '电力不足,资源生产已停止!',
deficitDetail: '电力缺口: {deficit},请建造更多电站',
buildSolarPlant: '建造电站'
},
planet: {
planet: '星球',
moon: '月球',
@@ -416,6 +425,7 @@ export default {
shipyardView: {
title: '船坞',
fleetStorage: '舰队仓储',
owned: '拥有',
attack: '攻击力',
shield: '护盾',
speed: '速度',
@@ -475,6 +485,9 @@ export default {
available: '可用',
all: '全部',
targetCoordinates: '目标坐标',
targetType: '目标类型',
planet: '行星',
moon: '月球',
galaxy: '银河系',
system: '星系',
position: '位置',
@@ -520,7 +533,47 @@ export default {
noDeathstar: '需要死星才能执行毁灭任务',
giftMode: '赠送模式',
giftModeDescription: '将资源作为礼物赠送给',
estimatedReputationGain: '预计好感度增加'
estimatedReputationGain: '预计好感度增加',
// 舰队预设
fleetPresets: '舰队预设',
fleetPresetsDescription: '保存常用的舰队配置快速派遣最多3个',
savePreset: '保存预设',
noPresets: '暂无预设,选择舰队后点击"保存预设"创建',
shipTypes: '种舰船',
editPreset: '编辑预设内容',
renamePreset: '重命名',
deletePreset: '删除预设',
editingPresetHint: '正在编辑预设,修改舰队配置后点击"保存"更新预设内容',
presetLimitReached: '预设数量已满',
presetLimitReachedMessage: '最多只能保存 {max} 个预设',
presetError: '保存失败',
presetNoShips: '请先选择至少一艘舰船',
presetDefaultName: '预设 {number}',
savePresetTitle: '保存舰队预设',
savePresetDescription: '为这个舰队配置命名',
renamePresetTitle: '重命名预设',
renamePresetDescription: '输入新的预设名称',
presetName: '预设名称',
presetNamePlaceholder: '输入预设名称',
deletePresetTitle: '删除预设',
deletePresetMessage: '确定要删除预设"{name}"吗?此操作不可撤销。',
// 跳跃门
jumpGate: '跳跃门',
jumpGateDescription: '使用跳跃门瞬间传送舰队到其他有跳跃门的月球',
jumpGateNotAvailable: '跳跃门不可用',
jumpGateRequiresMoon: '跳跃门只能在月球上使用',
jumpGateNotBuilt: '当前月球没有建造跳跃门',
jumpGateCooldown: '跳跃门冷却中',
jumpGateCooldownRemaining: '剩余冷却时间',
jumpGateReady: '跳跃门就绪',
jumpGateSelectTarget: '选择目标月球',
jumpGateNoTargetMoons: '没有可用的目标月球(需要有跳跃门且冷却完成)',
jumpGateSelectFleet: '选择传送舰队',
jumpGateTransfer: '传送舰队',
jumpGateSuccess: '跳跃门传送成功',
jumpGateSuccessMessage: '舰队已瞬间传送到 {target}',
jumpGateFailed: '跳跃门传送失败',
jumpGateFailedMessage: '请检查跳跃门状态和舰队配置'
},
officersView: {
title: '军官',
@@ -602,7 +655,26 @@ export default {
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。',
giftPlanetMessage: '确定要向星球 [{coordinates}] 赠送资源吗?\n\n请前往舰队页面选择运输船并装载资源。',
npcPlanetName: '{name}的星球'
npcPlanetName: '{name}的星球',
// 传感器阵列扫描
phalanxScan: '传感器扫描',
phalanxScanTitle: '传感器阵列扫描',
phalanxScanDescription: '扫描星球 [{coordinates}] 的舰队活动',
phalanxNoMoon: '需要有传感器阵列的月球才能扫描',
phalanxOutOfRange: '目标超出扫描范围',
phalanxRange: '扫描范围',
phalanxCost: '扫描消耗',
phalanxNoFleets: '未检测到舰队活动',
phalanxFleetDetected: '检测到 {count} 支舰队',
phalanxMission: '任务',
phalanxOrigin: '出发地',
phalanxDestination: '目的地',
phalanxArrival: '到达时间',
phalanxReturn: '返回时间',
phalanxStatus: '状态',
phalanxStatusOutbound: '前往中',
phalanxStatusReturning: '返回中',
phalanxInsufficientDeuterium: '氘不足'
},
messagesView: {
title: '消息中心',
@@ -718,14 +790,29 @@ export default {
missionReports: {
transportSuccess: '运输任务成功完成',
transportFailed: '运输任务失败',
transportFailedTargetNotFound: '运输失败:目标星球不存在',
transportFailedGiftRejected: '运输失败:礼物被拒绝',
colonizeSuccess: '殖民任务成功,新星球已建立',
colonizeFailed: '殖民任务失败',
colonizeFailedOccupied: '殖民失败:目标位置已被其他星球占用',
colonizeFailedMaxColonies: '殖民失败:已达到殖民地数量上限。研究天体物理学可增加上限。',
spySuccess: '侦查任务成功完成',
spyFailed: '侦查任务失败',
spyFailedTargetNotFound: '侦查失败:目标星球不存在',
deploySuccess: '部署任务成功完成',
deployFailed: '部署任务失败',
deployFailedTargetNotFound: '部署失败:目标星球不存在',
deployFailedNotOwnPlanet: '部署失败:目标星球不属于你',
recycleSuccess: '回收任务成功完成',
recycleFailed: '回收任务失败,目标位置没有残骸',
recycleFailedNoDebris: '回收失败:目标位置没有残骸场',
recycleFailedDebrisEmpty: '回收失败:残骸场已被清空',
destroySuccess: '行星毁灭任务成功执行',
destroyFailed: '行星毁灭任务失败',
destroyFailedTargetNotFound: '毁灭失败:目标星球不存在',
destroyFailedOwnPlanet: '毁灭失败:无法摧毁自己的星球',
destroyFailedNoDeathstar: '毁灭失败:没有死星执行任务',
destroyFailedChance: '毁灭失败:概率判定失败(成功率:{chance}%',
missileAttackSuccess: '导弹攻击成功',
missileAttackFailed: '导弹攻击失败,目标星球不存在',
missileAttackIntercepted: '所有导弹被拦截',
@@ -943,11 +1030,13 @@ export default {
missionType: {
spy: '侦查',
attack: '攻击',
recycle: '回收',
unknown: '未知'
},
warning: {
spy: '敌方侦查即将到达!',
attack: '敌方攻击即将到达!',
recycle: '敌方正在回收你附近的残骸!',
unknown: '敌方舰队即将到达!'
}
},
@@ -1336,6 +1425,10 @@ export default {
title: '战斗模拟器',
message: '在发动攻击前模拟战斗结果。输入双方舰队和科技等级,预测胜负和损失。'
},
achievements: {
title: '成就系统',
message: '完成各类游戏目标解锁成就,获得暗物质奖励!成就分为多个等级,挑战更高难度获得更丰厚的奖励。'
},
settings: {
title: '设置',
message: '在这里管理游戏数据、调整通知设置、导出/导入存档。记得定期备份你的进度!'
@@ -1344,5 +1437,102 @@ export default {
title: 'GM管理面板',
message: 'GM模式可以快速修改资源、建筑、科技等级。用于测试或体验完整游戏内容。'
}
},
achievements: {
title: '成就',
unlocked: '成就解锁',
progress: '进度',
nextTier: '下一等级',
maxTierReached: '已达最高等级',
tiers: {
bronze: '青铜',
silver: '白银',
gold: '黄金',
platinum: '铂金',
diamond: '钻石'
},
categories: {
resource: '资源',
building: '建造',
combat: '战斗',
mission: '任务',
diplomacy: '外交'
},
names: {
metalCollector: '金属收藏家',
crystalCollector: '晶体收藏家',
deuteriumCollector: '重氢收藏家',
darkMatterCollector: '暗物质收藏家',
resourceConsumer: '资源消耗者',
masterBuilder: '建造大师',
researcher: '科学家',
shipwright: '造船匠',
fortifier: '防御专家',
warmonger: '好战者',
conqueror: '征服者',
defender: '防御者',
fleetDestroyer: '舰队毁灭者',
debrisCreator: '残骸制造者',
fleetSacrifice: '舰队牺牲者',
defenseSacrifice: '防御牺牲者',
pilot: '飞行员',
transporter: '运输专家',
cargoMaster: '货运大师',
colonizer: '殖民者',
spy: '间谍大师',
deployer: '部署专家',
explorer: '探险家',
luckyExplorer: '幸运探险家',
recycler: '回收专家',
scavenger: '拾荒者',
destroyer: '毁灭者',
fuelBurner: '燃料消耗者',
diplomat: '外交官',
nemesis: '宿敌',
generous: '慷慨者',
philanthropist: '慈善家',
target: '目标',
watched: '被监视',
robbed: '被抢夺',
lostToNPC: '资源损失者'
},
descriptions: {
metalCollector: '累计生产金属',
crystalCollector: '累计生产晶体',
deuteriumCollector: '累计生产重氢',
darkMatterCollector: '累计生产暗物质',
resourceConsumer: '累计消耗资源',
masterBuilder: '累计升级建筑次数',
researcher: '累计完成研究次数',
shipwright: '累计生产舰船数量',
fortifier: '累计建造防御设施',
warmonger: '发起攻击次数',
conqueror: '攻击胜利次数',
defender: '防御成功次数',
fleetDestroyer: '防御中消灭的敌方舰队数量',
debrisCreator: '战斗中产生的残骸资源总量',
fleetSacrifice: '损失的舰队总数',
defenseSacrifice: '防御中损失的防御设施数量',
pilot: '完成飞行任务次数',
transporter: '运输任务次数',
cargoMaster: '运输资源总量',
colonizer: '成功殖民星球次数',
spy: '执行侦查任务次数',
deployer: '执行部署任务次数',
explorer: '远征总次数',
luckyExplorer: '远征成功次数',
recycler: '回收任务次数',
scavenger: '回收资源总量',
destroyer: '摧毁星球次数',
fuelBurner: '消耗燃料总量',
diplomat: '友好NPC数量',
nemesis: '敌对NPC数量',
generous: '送礼次数',
philanthropist: '赠送资源总量',
target: '被NPC攻击次数',
watched: '被NPC侦查次数',
robbed: '被NPC回收残骸次数',
lostToNPC: '被NPC回收的残骸资源总量'
}
}
}

View File

@@ -81,9 +81,11 @@ export default {
officers: '軍官',
simulator: '模擬',
galaxy: '星系',
diplomacy: 'Diplomacy',
diplomacy: '外交',
achievements: '成就',
messages: '訊息',
settings: '設定',
guide: '遊戲指南',
gm: 'GM'
},
sidebar: {
@@ -109,6 +111,14 @@ export default {
hour: '小時',
noEnergy: '電力不足'
},
energy: {
lowWarning: '電力不足,資源生產已停止!',
severeWarning: '電力不足,資源生產已停止!',
criticalWarning: '電力不足,資源生產已停止!',
noProduction: '電力不足,資源生產已停止!',
deficitDetail: '電力缺口: {deficit},請建造更多電站',
buildSolarPlant: '建造電站'
},
planet: {
planet: '星球',
moon: '月球',
@@ -422,6 +432,7 @@ export default {
shipyardView: {
title: '船塢',
fleetStorage: '艦隊倉儲',
owned: '擁有',
attack: '攻擊力',
missileAttack: '導彈攻擊',
shield: '護盾',
@@ -486,8 +497,10 @@ export default {
available: '可用',
all: '全部',
targetCoordinates: '目標座標',
targetType: '目標類型',
planet: '行星',
moon: '月球',
galaxy: '銀河系',
diplomacy: 'Diplomacy',
system: '星系',
position: '位置',
missionType: '任務類型',
@@ -531,7 +544,30 @@ export default {
noDeathstar: '需要死星才能執行毀滅任務',
giftMode: '贈送模式',
giftModeDescription: '將資源作為禮物贈送給',
estimatedReputationGain: '預計好感度增加'
estimatedReputationGain: '預計好感度增加',
// 艦隊預設
fleetPresets: '艦隊預設',
fleetPresetsDescription: '儲存常用的艦隊配置快速派遣最多3個',
savePreset: '儲存預設',
noPresets: '暫無預設,選擇艦隊後點擊「儲存預設」建立',
shipTypes: '種艦船',
editPreset: '編輯預設內容',
renamePreset: '重新命名',
deletePreset: '刪除預設',
editingPresetHint: '正在編輯預設,修改艦隊配置後點擊「儲存」更新預設內容',
presetLimitReached: '預設數量已滿',
presetLimitReachedMessage: '最多只能儲存 {max} 個預設',
presetError: '儲存失敗',
presetNoShips: '請先選擇至少一艘艦船',
presetDefaultName: '預設 {number}',
savePresetTitle: '儲存艦隊預設',
savePresetDescription: '為這個艦隊配置命名',
renamePresetTitle: '重新命名預設',
renamePresetDescription: '輸入新的預設名稱',
presetName: '預設名稱',
presetNamePlaceholder: '輸入預設名稱',
deletePresetTitle: '刪除預設',
deletePresetMessage: '確定要刪除預設「{name}」嗎?此操作不可撤銷。'
},
officersView: {
title: '軍官',
@@ -571,7 +607,6 @@ export default {
title: '星系',
selectCoordinates: '選擇座標',
galaxy: '銀河系',
diplomacy: 'Diplomacy',
selectGalaxy: '選擇銀河系',
system: '星系',
selectSystem: '選擇星系',
@@ -728,14 +763,29 @@ export default {
missionReports: {
transportSuccess: '運輸任務成功完成',
transportFailed: '運輸任務失敗',
transportFailedTargetNotFound: '運輸失敗:目標星球不存在',
transportFailedGiftRejected: '運輸失敗:禮物被拒絕',
colonizeSuccess: '殖民任務成功,新星球已建立',
colonizeFailed: '殖民任務失敗',
colonizeFailedOccupied: '殖民失敗:目標位置已被其他星球佔用',
colonizeFailedMaxColonies: '殖民失敗:已達到殖民地數量上限。研究天體物理學可增加上限。',
spySuccess: '偵查任務成功完成',
spyFailed: '偵查任務失敗',
spyFailedTargetNotFound: '偵查失敗:目標星球不存在',
deploySuccess: '部署任務成功完成',
deployFailed: '部署任務失敗',
deployFailedTargetNotFound: '部署失敗:目標星球不存在',
deployFailedNotOwnPlanet: '部署失敗:目標星球不屬於你',
recycleSuccess: '回收任務成功完成',
recycleFailed: '回收任務失敗,目標位置沒有殘骸',
recycleFailedNoDebris: '回收失敗:目標位置沒有殘骸場',
recycleFailedDebrisEmpty: '回收失敗:殘骸場已被清空',
destroySuccess: '行星毀滅任務成功執行',
destroyFailed: '行星毀滅任務失敗',
destroyFailedTargetNotFound: '毀滅失敗:目標星球不存在',
destroyFailedOwnPlanet: '毀滅失敗:無法摧毀自己的星球',
destroyFailedNoDeathstar: '毀滅失敗:沒有死星執行任務',
destroyFailedChance: '毀滅失敗:概率判定失敗(成功率:{chance}%',
expeditionResources: '探險隊發現了資源!',
expeditionDarkMatter: '探險隊發現了暗物質!',
expeditionFleet: '探險隊發現了廢棄的艦船!',
@@ -948,11 +998,13 @@ export default {
missionType: {
spy: '偵查',
attack: '攻擊',
recycle: '回收',
unknown: '未知'
},
warning: {
spy: '敵方偵查即將到達!',
attack: '敵方攻擊即將到達!',
recycle: '敵方正在回收你附近的殘骸!',
unknown: '敵方艦隊即將到達!'
}
},
@@ -1235,6 +1287,10 @@ export default {
title: '戰鬥模擬器',
message: '在發動攻擊前模擬戰鬥結果。輸入雙方艦隊和科技等級,預測勝負和損失。'
},
achievements: {
title: '成就系統',
message: '完成各類遊戲目標解鎖成就,獲得暗物質獎勵!成就分為多個等級,挑戰更高難度獲得更豐厚的獎勵。'
},
settings: {
title: '設置',
message: '在這裡管理遊戲數據、調整通知設置、導出/導入存檔。記得定期備份你的進度!'
@@ -1243,5 +1299,239 @@ export default {
title: 'GM管理面板',
message: 'GM模式可以快速修改資源、建築、科技等級。用於測試或體驗完整遊戲內容。'
}
},
achievements: {
title: '成就',
unlocked: '成就解鎖',
progress: '進度',
nextTier: '下一等級',
maxTierReached: '已達最高等級',
tiers: {
bronze: '青銅',
silver: '白銀',
gold: '黃金',
platinum: '白金',
diamond: '鑽石'
},
categories: {
resource: '資源',
building: '建造',
combat: '戰鬥',
mission: '任務',
diplomacy: '外交'
},
names: {
metalCollector: '金屬收藏家',
crystalCollector: '晶體收藏家',
deuteriumCollector: '重氫收藏家',
darkMatterCollector: '暗物質收藏家',
resourceConsumer: '資源消耗者',
masterBuilder: '建造大師',
researcher: '科學家',
shipwright: '造船匠',
fortifier: '防禦專家',
warmonger: '好戰者',
conqueror: '征服者',
defender: '防禦者',
fleetDestroyer: '艦隊毀滅者',
debrisCreator: '殘骸製造者',
fleetSacrifice: '艦隊犧牲者',
defenseSacrifice: '防禦犧牲者',
pilot: '飛行員',
transporter: '運輸專家',
cargoMaster: '貨運大師',
colonizer: '殖民者',
spy: '間諜大師',
deployer: '部署專家',
explorer: '探險家',
luckyExplorer: '幸運探險家',
recycler: '回收專家',
scavenger: '拾荒者',
destroyer: '毀滅者',
fuelBurner: '燃料消耗者',
diplomat: '外交官',
nemesis: '宿敵',
generous: '慷慨者',
philanthropist: '慈善家',
target: '目標',
watched: '被監視',
robbed: '被搶奪',
lostToNPC: '資源損失者'
},
descriptions: {
metalCollector: '累計生產金屬',
crystalCollector: '累計生產晶體',
deuteriumCollector: '累計生產重氫',
darkMatterCollector: '累計生產暗物質',
resourceConsumer: '累計消耗資源',
masterBuilder: '累計升級建築次數',
researcher: '累計完成研究次數',
shipwright: '累計生產艦船數量',
fortifier: '累計建造防禦設施',
warmonger: '發起攻擊次數',
conqueror: '攻擊勝利次數',
defender: '防禦成功次數',
fleetDestroyer: '防禦中消滅的敵方艦隊數量',
debrisCreator: '戰鬥中產生的殘骸資源總量',
fleetSacrifice: '損失的艦隊總數',
defenseSacrifice: '防禦中損失的防禦設施數量',
pilot: '完成飛行任務次數',
transporter: '運輸任務次數',
cargoMaster: '運輸資源總量',
colonizer: '成功殖民星球次數',
spy: '執行偵查任務次數',
deployer: '執行部署任務次數',
explorer: '遠征總次數',
luckyExplorer: '遠征成功次數',
recycler: '回收任務次數',
scavenger: '回收資源總量',
destroyer: '摧毀星球次數',
fuelBurner: '消耗燃料總量',
diplomat: '友好NPC數量',
nemesis: '敵對NPC數量',
generous: '送禮次數',
philanthropist: '贈送資源總量',
target: '被NPC攻擊次數',
watched: '被NPC偵查次數',
robbed: '被NPC回收殘骸次數',
lostToNPC: '被NPC回收的殘骸資源總量'
}
},
guide: {
title: '遊戲指南',
tableOfContents: '目錄',
quickstart: {
title: '新手入門',
description: '快速了解遊戲核心機制,開始你的星際征程',
step1: {
title: '建造資源礦場',
content1: '資源是發展的基礎。首先建造金屬礦、晶體礦和重氫合成器。',
item1: '金屬礦 - 產出金屬,是最基礎的資源',
item2: '晶體礦 - 產出晶體,用於高級建築和艦船',
item3: '重氫合成器 - 產出重氫,用於燃料和高級科技',
item4: '建議優先發展金屬礦,其次晶體礦'
},
step2: {
title: '確保能源供應',
content1: '礦場需要電力才能運作。建造太陽能電站提供電力。',
item1: '太陽能電站 - 提供基礎電力,無需消耗資源',
item2: '核聚變反應堆 - 後期可用,消耗重氫但產電量更高'
},
step3: {
title: '建設基礎設施',
content1: '設施是發展的重要支撐。',
item1: '機器人工廠 - 加速所有建築的建造速度',
item2: '船塢 - 用於建造艦船和防禦設施',
item3: '研究實驗室 - 用於研發科技'
},
step4: {
title: '研發科技',
content1: '科技可以解鎖更多內容並提供加成。',
item1: '能量技術 - 解鎖更多科技的前置',
item2: '燃燒引擎 - 提高艦船速度',
item3: '電腦技術 - 增加可同時執行的艦隊任務數'
},
step5: {
title: '建造艦隊',
content1: '艦隊是探索和戰鬥的關鍵。',
item1: '小型貨船 - 用於運輸資源',
item2: '輕型戰鬥機 - 基礎戰鬥單位',
item3: '間諜衛星 - 偵查敵人情報'
},
step6: {
title: '探索宇宙',
content1: '使用艦隊探索宇宙,獲取資源和發展。',
item1: '偵查 - 獲取敵人情報',
item2: '攻擊 - 掠奪資源',
item3: '運輸 - 在星球間轉移資源',
item4: '殖民 - 建立新的殖民地'
}
},
buildings: {
title: '建築',
description: '了解遊戲中所有建築的功能和作用',
resource: { title: '資源建築' },
energy: { title: '能源建築' },
facility: { title: '設施建築' },
storage: { title: '倉儲建築' },
moon: { title: '月球建築' }
},
research: {
title: '研究',
description: '了解所有科技的功能和解鎖條件',
basic: { title: '基礎科技' },
drive: { title: '驅動科技' },
combat: { title: '戰鬥科技' },
advanced: { title: '高級科技' }
},
ships: {
title: '艦船',
description: '了解各類艦船的屬性和用途',
combat: { title: '戰鬥艦船' },
civil: { title: '民用艦船' },
special: { title: '特殊艦船' },
stats: '攻擊: {attack} | 護盾: {shield} | 速度: {speed}',
cargo: '載貨量: {cargo}'
},
defense: {
title: '防禦',
description: '了解各類防禦設施的屬性和作用',
basic: { title: '基礎防禦' },
advanced: { title: '高級防禦' },
shields: { title: '護盾與導彈' },
stats: '攻擊: {attack} | 護盾: {shield}'
},
systems: {
title: '遊戲系統',
description: '了解遊戲的核心系統機制',
resource: {
title: '資源系統',
content1: '資源是遊戲的核心,用於建造建築、研發科技和生產艦船。',
item1: '金屬 - 最基礎的資源,產量最高',
item2: '晶體 - 用於高級建築和艦船',
item3: '重氫 - 用於燃料和高級科技',
item4: '暗物質 - 稀有資源,用於購買軍官和特殊功能'
},
combat: {
title: '戰鬥系統',
content1: '戰鬥採用回合制,每輪艦船和防禦設施都會互相攻擊。',
item1: '攻擊力 - 決定對敵方造成的傷害',
item2: '護盾值 - 吸收部分傷害',
item3: '結構值 - 單位的生命值',
item4: '速射 - 某些艦船可以在一輪內多次攻擊特定目標'
},
missions: {
title: '艦隊任務',
content1: '艦隊可以執行多種類型的任務。',
item1: '攻擊 - 攻擊敵方星球,掠奪資源',
item2: '運輸 - 在自己的星球間運送資源',
item3: '部署 - 將艦隊永久轉移到另一個星球',
item4: '殖民 - 使用殖民船建立新殖民地',
item5: '偵查 - 使用間諜衛星獲取情報',
item6: '回收 - 回收戰場殘骸中的資源'
},
diplomacy: {
title: '外交系統',
content1: 'NPC勢力具有好感度系統影響他們對你的態度。',
item1: '友好 - NPC不會主動攻擊你',
item2: '中立 - NPC可能會偵查你',
item3: '敵對 - NPC會主動攻擊你的星球'
},
achievements: {
title: '成就系統',
content1: '完成各類遊戲目標解鎖成就,獲得暗物質獎勵。',
item1: '成就分為多個等級,難度遞增',
item2: '完成成就可獲得暗物質獎勵',
item3: '成就進度會自動追蹤'
},
officers: {
title: '軍官系統',
content1: '雇傭軍官可以獲得各種加成效果。',
item1: '指揮官 - 減少建造時間',
item2: '提督 - 增加艦隊任務槽位',
item3: '工程師 - 減少防禦修復時間',
item4: '地質學家 - 增加資源產量'
}
}
}
}

View File

@@ -0,0 +1,463 @@
import {
AchievementTier,
BuildingType,
TechnologyType,
ShipType,
DefenseType,
type AchievementStats,
type AchievementProgress,
type AchievementReward,
type Player,
type Resources,
type BattleResult
} from '@/types/game'
import { ACHIEVEMENTS, ACHIEVEMENT_MAP, getNextTier, getTierIndex } from '@/config/achievementConfig'
// 初始化空的成就统计数据
export function initializeAchievementStats(): AchievementStats {
const emptyShipRecord = Object.values(ShipType).reduce((acc, type) => {
acc[type] = 0
return acc
}, {} as Record<ShipType, number>)
const emptyDefenseRecord = Object.values(DefenseType).reduce((acc, type) => {
acc[type] = 0
return acc
}, {} as Record<DefenseType, number>)
const emptyBuildingRecord = Object.values(BuildingType).reduce((acc, type) => {
acc[type] = 0
return acc
}, {} as Record<BuildingType, number>)
const emptyTechRecord = Object.values(TechnologyType).reduce((acc, type) => {
acc[type] = 0
return acc
}, {} as Record<TechnologyType, number>)
return {
// 资源统计
totalMetalProduced: 0,
totalCrystalProduced: 0,
totalDeuteriumProduced: 0,
totalDarkMatterProduced: 0,
totalMetalConsumed: 0,
totalCrystalConsumed: 0,
totalDeuteriumConsumed: 0,
totalDarkMatterConsumed: 0,
// 建造统计
buildingsUpgraded: 0,
maxBuildingLevel: { ...emptyBuildingRecord },
researchCompleted: 0,
maxTechnologyLevel: { ...emptyTechRecord },
shipsProduced: { ...emptyShipRecord },
totalShipsProduced: 0,
defensesBuilt: { ...emptyDefenseRecord },
totalDefensesBuilt: 0,
// 战斗统计
attacksLaunched: 0,
attacksWon: 0,
attacksLost: 0,
fleetLostInAttack: { ...emptyShipRecord },
totalFleetLostInAttack: 0,
debrisCreatedFromAttacks: 0,
defensesSuccessful: 0,
defensesFailed: 0,
fleetLostInDefense: { ...emptyShipRecord },
totalFleetLostInDefense: 0,
defenseLostInDefense: { ...emptyDefenseRecord },
totalDefenseLostInDefense: 0,
enemyFleetDestroyedInDefense: { ...emptyShipRecord },
totalEnemyFleetDestroyedInDefense: 0,
debrisCreatedFromDefenses: 0,
// 任务统计
totalFlightMissions: 0,
transportMissions: 0,
transportedResources: 0,
colonizations: 0,
spyMissions: 0,
deployments: 0,
expeditionsTotal: 0,
expeditionsSuccessful: 0,
recyclingMissions: 0,
recycledResources: 0,
planetDestructions: 0,
fuelConsumed: 0,
// 外交统计
friendlyNPCCount: 0,
hostileNPCCount: 0,
giftsSent: 0,
giftResourcesTotal: 0,
attackedByNPC: 0,
spiedByNPC: 0,
debrisRecycledByNPC: 0,
debrisResourcesLostToNPC: 0
}
}
// 初始化所有成就进度
export function initializeAchievements(): Record<string, AchievementProgress> {
const achievements: Record<string, AchievementProgress> = {}
for (const config of ACHIEVEMENTS) {
achievements[config.id] = {
id: config.id,
currentTier: null,
currentValue: 0,
tierUnlocks: {
[AchievementTier.Bronze]: null,
[AchievementTier.Silver]: null,
[AchievementTier.Gold]: null,
[AchievementTier.Platinum]: null,
[AchievementTier.Diamond]: null
}
}
}
return achievements
}
// 获取成就的当前值
function getAchievementValue(stats: AchievementStats, statKey: string, checkType: string): number {
// 处理特殊的组合统计键
if (statKey === 'totalResourcesConsumed') {
return stats.totalMetalConsumed + stats.totalCrystalConsumed + stats.totalDeuteriumConsumed + stats.totalDarkMatterConsumed
}
if (statKey === 'totalDebrisCreated') {
return stats.debrisCreatedFromAttacks + stats.debrisCreatedFromDefenses
}
if (statKey === 'totalFleetLost') {
return stats.totalFleetLostInAttack + stats.totalFleetLostInDefense
}
// 处理普通统计键
const value = stats[statKey as keyof AchievementStats]
if (typeof value === 'number') {
return value
}
// 处理 Record 类型的值(如果需要求和)
if (checkType === 'sum' && typeof value === 'object') {
return Object.values(value as Record<string, number>).reduce((a, b) => a + b, 0)
}
return 0
}
// 检查并更新成就进度,返回新解锁的成就
export interface AchievementUnlock {
id: string
tier: AchievementTier
reward: AchievementReward
}
export function checkAchievements(player: Player): AchievementUnlock[] {
if (!player.achievementStats || !player.achievements) {
return []
}
const unlocks: AchievementUnlock[] = []
const stats = player.achievementStats
const achievements = player.achievements
for (const config of ACHIEVEMENTS) {
const progress = achievements[config.id]
if (!progress) continue
// 获取当前值
const currentValue = getAchievementValue(stats, config.statKey, config.checkType)
progress.currentValue = currentValue
// 检查每个等级
for (const tierConfig of config.tiers) {
// 如果已经解锁这个等级,跳过
if (progress.tierUnlocks[tierConfig.tier] !== null) continue
// 检查是否达到目标
let achieved = false
if (config.checkType === 'gte' || config.checkType === 'sum') {
achieved = currentValue >= tierConfig.target
} else if (config.checkType === 'eq') {
achieved = currentValue === tierConfig.target
} else if (config.checkType === 'max') {
achieved = currentValue >= tierConfig.target
}
if (achieved) {
// 解锁成就
const now = Date.now()
progress.tierUnlocks[tierConfig.tier] = now
// 更新当前最高等级
if (progress.currentTier === null || getTierIndex(tierConfig.tier) > getTierIndex(progress.currentTier)) {
progress.currentTier = tierConfig.tier
progress.unlockedAt = now
}
unlocks.push({
id: config.id,
tier: tierConfig.tier,
reward: tierConfig.reward
})
}
}
}
return unlocks
}
// 应用成就奖励
export function applyAchievementReward(player: Player, reward: AchievementReward): void {
const firstPlanet = player.planets[0]
if (reward.darkMatter && firstPlanet) {
// 奖励添加到第一个星球的资源中
firstPlanet.resources.darkMatter += reward.darkMatter
}
if (reward.points) {
player.points += reward.points
}
}
// ==================== 统计更新函数 ====================
// 更新资源生产统计
export function updateResourceProductionStats(
stats: AchievementStats,
produced: { metal?: number; crystal?: number; deuterium?: number; darkMatter?: number }
): void {
if (produced.metal) stats.totalMetalProduced += produced.metal
if (produced.crystal) stats.totalCrystalProduced += produced.crystal
if (produced.deuterium) stats.totalDeuteriumProduced += produced.deuterium
if (produced.darkMatter) stats.totalDarkMatterProduced += produced.darkMatter
}
// 更新资源消耗统计
export function updateResourceConsumptionStats(stats: AchievementStats, consumed: Partial<Resources>): void {
if (consumed.metal) stats.totalMetalConsumed += consumed.metal
if (consumed.crystal) stats.totalCrystalConsumed += consumed.crystal
if (consumed.deuterium) stats.totalDeuteriumConsumed += consumed.deuterium
if (consumed.darkMatter) stats.totalDarkMatterConsumed += consumed.darkMatter
}
// 更新建筑升级统计
export function updateBuildingStats(stats: AchievementStats, buildingType: BuildingType, level: number): void {
stats.buildingsUpgraded += 1
// 更新最高等级记录
if (level > (stats.maxBuildingLevel[buildingType] || 0)) {
stats.maxBuildingLevel[buildingType] = level
}
}
// 更新科技研究统计
export function updateResearchStats(stats: AchievementStats, techType: TechnologyType, level: number): void {
stats.researchCompleted += 1
// 更新最高等级记录
if (level > (stats.maxTechnologyLevel[techType] || 0)) {
stats.maxTechnologyLevel[techType] = level
}
}
// 更新舰船生产统计
export function updateShipProductionStats(stats: AchievementStats, shipType: ShipType, quantity: number): void {
stats.shipsProduced[shipType] = (stats.shipsProduced[shipType] || 0) + quantity
stats.totalShipsProduced += quantity
}
// 更新防御建造统计
export function updateDefenseProductionStats(stats: AchievementStats, defenseType: DefenseType, quantity: number): void {
stats.defensesBuilt[defenseType] = (stats.defensesBuilt[defenseType] || 0) + quantity
stats.totalDefensesBuilt += quantity
}
// 更新攻击统计(玩家作为攻击者)
export function updateAttackStats(stats: AchievementStats, battleResult: BattleResult, won: boolean, debrisValue: number): void {
// 攻击也算飞行任务
stats.totalFlightMissions += 1
stats.attacksLaunched += 1
if (won) {
stats.attacksWon += 1
} else if (battleResult.winner === 'defender') {
stats.attacksLost += 1
}
// 记录攻击中损失的舰队
if (battleResult.attackerLosses) {
for (const [shipType, count] of Object.entries(battleResult.attackerLosses)) {
if (count && count > 0) {
stats.fleetLostInAttack[shipType as ShipType] = (stats.fleetLostInAttack[shipType as ShipType] || 0) + count
stats.totalFleetLostInAttack += count
}
}
}
// 记录产生的残骸
stats.debrisCreatedFromAttacks += debrisValue
}
// 更新防御统计(玩家作为防御者)
export function updateDefenseStats(stats: AchievementStats, battleResult: BattleResult, won: boolean, debrisValue: number): void {
if (won) {
stats.defensesSuccessful += 1
} else {
stats.defensesFailed += 1
}
// 记录防御中损失的舰队
if (battleResult.defenderLosses?.fleet) {
for (const [shipType, count] of Object.entries(battleResult.defenderLosses.fleet)) {
if (count && count > 0) {
stats.fleetLostInDefense[shipType as ShipType] = (stats.fleetLostInDefense[shipType as ShipType] || 0) + count
stats.totalFleetLostInDefense += count
}
}
}
// 记录防御中损失的防御设施
if (battleResult.defenderLosses?.defense) {
for (const [defenseType, count] of Object.entries(battleResult.defenderLosses.defense)) {
if (count && count > 0) {
stats.defenseLostInDefense[defenseType as DefenseType] = (stats.defenseLostInDefense[defenseType as DefenseType] || 0) + count
stats.totalDefenseLostInDefense += count
}
}
}
// 记录防御中消灭的敌方舰队
if (battleResult.attackerLosses) {
for (const [shipType, count] of Object.entries(battleResult.attackerLosses)) {
if (count && count > 0) {
stats.enemyFleetDestroyedInDefense[shipType as ShipType] = (stats.enemyFleetDestroyedInDefense[shipType as ShipType] || 0) + count
stats.totalEnemyFleetDestroyedInDefense += count
}
}
}
// 记录产生的残骸
stats.debrisCreatedFromDefenses += debrisValue
}
// 更新飞行任务统计
export function updateFlightMissionStats(stats: AchievementStats): void {
stats.totalFlightMissions += 1
}
// 更新运输任务统计
export function updateTransportStats(stats: AchievementStats, resourcesAmount: number): void {
stats.transportMissions += 1
stats.transportedResources += resourcesAmount
}
// 更新殖民统计
export function updateColonizationStats(stats: AchievementStats): void {
stats.colonizations += 1
}
// 更新侦查统计
export function updateSpyStats(stats: AchievementStats): void {
stats.spyMissions += 1
}
// 更新部署统计
export function updateDeploymentStats(stats: AchievementStats): void {
stats.deployments += 1
}
// 更新探险统计
export function updateExpeditionStats(stats: AchievementStats, successful: boolean): void {
stats.expeditionsTotal += 1
if (successful) {
stats.expeditionsSuccessful += 1
}
}
// 更新回收统计
export function updateRecyclingStats(stats: AchievementStats, resourcesAmount: number): void {
stats.recyclingMissions += 1
stats.recycledResources += resourcesAmount
}
// 更新行星毁灭统计
export function updatePlanetDestructionStats(stats: AchievementStats): void {
stats.planetDestructions += 1
}
// 更新燃料消耗统计
export function updateFuelConsumptionStats(stats: AchievementStats, fuelAmount: number): void {
stats.fuelConsumed += fuelAmount
}
// 更新友好NPC数量
export function updateFriendlyNPCStats(stats: AchievementStats, count: number): void {
stats.friendlyNPCCount = count
}
// 更新敌对NPC数量
export function updateHostileNPCStats(stats: AchievementStats, count: number): void {
stats.hostileNPCCount = count
}
// 更新送礼统计
export function updateGiftStats(stats: AchievementStats, resourcesAmount: number): void {
stats.giftsSent += 1
stats.giftResourcesTotal += resourcesAmount
}
// 更新被NPC攻击统计
export function updateAttackedByNPCStats(stats: AchievementStats): void {
stats.attackedByNPC += 1
}
// 更新被NPC侦查统计
export function updateSpiedByNPCStats(stats: AchievementStats): void {
stats.spiedByNPC += 1
}
// 更新被NPC回收残骸统计
export function updateDebrisRecycledByNPCStats(stats: AchievementStats, resourcesAmount: number): void {
stats.debrisRecycledByNPC += 1
stats.debrisResourcesLostToNPC += resourcesAmount
}
// 获取成就的下一个目标
export function getNextTarget(achievementId: string, currentTier: AchievementTier | null): number | null {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return null
const nextTier = getNextTier(currentTier)
if (!nextTier) return null
const tierConfig = config.tiers.find(t => t.tier === nextTier)
return tierConfig?.target ?? null
}
// 获取成就进度百分比
export function getAchievementProgress(achievementId: string, currentValue: number, currentTier: AchievementTier | null): number {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return 0
const nextTier = getNextTier(currentTier)
if (!nextTier) return 100 // 已达到最高等级
const tierConfig = config.tiers.find(t => t.tier === nextTier)
if (!tierConfig) return 0
// 计算当前等级的起始值
let startValue = 0
if (currentTier) {
const currentTierConfig = config.tiers.find(t => t.tier === currentTier)
startValue = currentTierConfig?.target ?? 0
}
const progress = ((currentValue - startValue) / (tierConfig.target - startValue)) * 100
return Math.min(100, Math.max(0, progress))
}

View File

@@ -73,6 +73,21 @@ export const createFleetMission = (
}
}
/**
* 运输任务失败原因
*/
export type TransportFailReason = 'targetNotFound' | 'giftRejected'
/**
* 运输任务结果
*/
export interface TransportResult {
success: boolean
reputationGain?: number
overflow?: Resources
failReason?: TransportFailReason
}
/**
* 处理运输任务到达
*/
@@ -83,7 +98,7 @@ export const processTransportArrival = (
allNpcs?: NPC[],
locale: Locale = 'zh-CN',
storageCapacityBonus: number = 0
): { success: boolean; reputationGain?: number; overflow?: Resources } => {
): TransportResult => {
// 检查是否是赠送任务
if (mission.isGift && mission.giftTargetNpcId && player && allNpcs) {
const targetNpc = allNpcs.find(n => n.id === mission.giftTargetNpcId)
@@ -94,7 +109,7 @@ export const processTransportArrival = (
// 如果礼物被拒绝,资源返还给玩家
if (!giftResult.accepted) {
// 资源保留在cargo中返回时会退还给玩家
return { success: false, reputationGain: undefined }
return { success: false, reputationGain: undefined, failReason: 'giftRejected' }
}
// 礼物被接受,清空货物
@@ -114,10 +129,13 @@ export const processTransportArrival = (
mission.cargo = result.overflow
return { success: true, overflow: result.overflow }
}
// 运输成功,清空货物
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
return { success: true }
}
// 目标星球不存在
mission.status = 'returning'
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
return { success: false }
return { success: false, failReason: 'targetNotFound' }
}
/**
@@ -356,6 +374,15 @@ export const canColonize = (player: Player): boolean => {
return currentPlanets < maxPlanets
}
/**
* 殖民任务结果
*/
export interface ColonizeResult {
success: boolean
planet: Planet | null
failReason?: 'positionOccupied' | 'maxColoniesReached'
}
/**
* 处理殖民任务到达
*/
@@ -364,18 +391,18 @@ export const processColonizeArrival = (
targetPlanet: Planet | undefined,
player: Player,
colonyNameTemplate: string = 'Colony'
): Planet | null => {
): ColonizeResult => {
if (targetPlanet) {
// 位置已被占用
mission.status = 'returning'
return null
return { success: false, planet: null, failReason: 'positionOccupied' }
}
// 检查殖民地槽位限制
if (!canColonize(player)) {
// 超出殖民地数量限制,殖民船返回
mission.status = 'returning'
return null
return { success: false, planet: null, failReason: 'maxColoniesReached' }
}
// 创建新殖民地
@@ -431,7 +458,7 @@ export const processColonizeArrival = (
mission.fleet[ShipType.ColonyShip] = (mission.fleet[ShipType.ColonyShip] || 1) - 1
mission.status = 'returning'
return newPlanet
return { success: true, planet: newPlanet }
}
/**
@@ -481,6 +508,20 @@ export const calculateSpyDetectionChance = (attackerSpyLevel: number, defenderSp
return Math.max(0.01, Math.min(0.99, baseChance))
}
/**
* 侦查任务失败原因
*/
export type SpyFailReason = 'targetNotFound'
/**
* 侦查任务结果
*/
export interface SpyResult {
success: boolean
report?: SpyReport
failReason?: SpyFailReason
}
/**
* 处理间谍任务到达
*/
@@ -491,10 +532,10 @@ export const processSpyArrival = (
defender: Player | null,
allNpcs?: NPC[],
locale: Locale = 'zh-CN'
): SpyReport | null => {
): SpyResult => {
if (!targetPlanet) {
mission.status = 'returning'
return null
return { success: false, failReason: 'targetNotFound' }
}
// 获取间谍技术等级
@@ -536,7 +577,21 @@ export const processSpyArrival = (
}
mission.status = 'returning'
return spyReport
return { success: true, report: spyReport }
}
/**
* 部署任务失败原因
*/
export type DeployFailReason = 'targetNotFound' | 'notOwnPlanet'
/**
* 部署任务结果
*/
export interface DeployResult {
success: boolean
overflow?: Partial<Record<ShipType, number>>
failReason?: DeployFailReason
}
/**
@@ -547,10 +602,14 @@ export const processDeployArrival = (
targetPlanet: Planet | undefined,
playerId: string,
technologies: Record<TechnologyType, number>
): { success: boolean; overflow?: Partial<Record<ShipType, number>> } => {
if (!targetPlanet || targetPlanet.ownerId !== playerId) {
): DeployResult => {
if (!targetPlanet) {
mission.status = 'returning'
return { success: false }
return { success: false, failReason: 'targetNotFound' }
}
if (targetPlanet.ownerId !== playerId) {
mission.status = 'returning'
return { success: false, failReason: 'notOwnPlanet' }
}
// 使用安全添加函数,防止舰队仓储溢出
@@ -566,6 +625,21 @@ export const processDeployArrival = (
return { success: true }
}
/**
* 回收任务失败原因
*/
export type RecycleFailReason = 'noDebrisField' | 'debrisEmpty'
/**
* 回收任务结果
*/
export interface RecycleResult {
success: boolean
collectedResources?: Pick<Resources, 'metal' | 'crystal'>
remainingDebris?: Pick<Resources, 'metal' | 'crystal'> | null
failReason?: RecycleFailReason
}
/**
* 处理回收任务到达
*/
@@ -575,10 +649,10 @@ export const processRecycleArrival = (
player?: Player,
allNpcs?: NPC[],
locale: Locale = 'zh-CN'
): { collectedResources: Pick<Resources, 'metal' | 'crystal'>; remainingDebris: Pick<Resources, 'metal' | 'crystal'> | null } | null => {
): RecycleResult => {
if (!debrisField) {
mission.status = 'returning'
return null
return { success: false, failReason: 'noDebrisField' }
}
// 计算回收船的货舱容量
@@ -599,7 +673,7 @@ export const processRecycleArrival = (
// 防止除零如果残骸为0直接返回
if (totalDebris === 0) {
mission.status = 'returning'
return null
return { success: false, failReason: 'debrisEmpty' }
}
// 按比例收集金属和晶体
@@ -625,6 +699,7 @@ export const processRecycleArrival = (
}
return {
success: true,
collectedResources: {
metal: collectedMetal,
crystal: collectedCrystal
@@ -840,42 +915,114 @@ export const calculatePlanetDefensePower = (fleet: Partial<Fleet>, defense: Part
}
/**
* 处理行星毁灭任务到达
* 处理行星/月球毁灭任务到达
* OGame规则
* - 月球销毁概率 = (100 - √diameter) × √deathstars
* - 死星反向销毁概率 = √diameter / 2
* - 两个概率独立判定
*/
/**
* 毁灭任务失败原因
*/
export type DestroyFailReason = 'targetNotFound' | 'ownPlanet' | 'noDeathstar' | 'chanceFailed'
/**
* 销毁任务结果
*/
export interface DestroyResult {
success: boolean // 目标是否被销毁
destructionChance: number // 销毁概率
planetId?: string // 被销毁的星球/月球ID
deathstarsLost: boolean // 死星是否被反向销毁
deathstarDestructionChance: number // 死星反向销毁概率
isMoon: boolean // 目标是否为月球
failReason?: DestroyFailReason // 失败原因
}
export const processDestroyArrival = (
mission: FleetMission,
targetPlanet: Planet | undefined,
attacker: Player
): { success: boolean; destructionChance: number; planetId?: string } | null => {
if (!targetPlanet || targetPlanet.ownerId === attacker.id) {
): DestroyResult => {
if (!targetPlanet) {
mission.status = 'returning'
return null
return {
success: false,
destructionChance: 0,
deathstarsLost: false,
deathstarDestructionChance: 0,
isMoon: false,
failReason: 'targetNotFound'
}
}
if (targetPlanet.ownerId === attacker.id) {
mission.status = 'returning'
return {
success: false,
destructionChance: 0,
deathstarsLost: false,
deathstarDestructionChance: 0,
isMoon: targetPlanet.isMoon || false,
failReason: 'ownPlanet'
}
}
// 检查是否有死星
const deathstarCount = mission.fleet[ShipType.Deathstar] || 0
if (deathstarCount === 0) {
mission.status = 'returning'
return null
return {
success: false,
destructionChance: 0,
deathstarsLost: false,
deathstarDestructionChance: 0,
isMoon: targetPlanet.isMoon || false,
failReason: 'noDeathstar'
}
}
// 计算目标星球的防御力量
const planetaryShieldCount = targetPlanet.defense[DefenseType.PlanetaryShield] || 0
const defensePower = calculatePlanetDefensePower(targetPlanet.fleet, targetPlanet.defense)
// 根据目标类型使用不同的销毁逻辑
if (targetPlanet.isMoon) {
// 月球销毁使用 OGame 公式
const result = moonLogic.tryDestroyMoon(targetPlanet, deathstarCount)
// 计算摧毁概率
const destructionChance = calculateDestructionChance(deathstarCount, planetaryShieldCount, defensePower)
// 如果死星被反向销毁,从任务舰队中移除
if (result.deathstarsDestroyed) {
mission.fleet[ShipType.Deathstar] = 0
}
// 随机判断是否成功
const randomValue = Math.random() * 100
const success = randomValue < destructionChance
mission.status = 'returning'
mission.status = 'returning'
return {
success: result.moonDestroyed,
destructionChance: result.moonDestructionChance,
planetId: result.moonDestroyed ? targetPlanet.id : undefined,
deathstarsLost: result.deathstarsDestroyed,
deathstarDestructionChance: result.deathstarDestructionChance,
isMoon: true,
failReason: result.moonDestroyed ? undefined : 'chanceFailed'
}
} else {
// 行星销毁使用原有逻辑
const planetaryShieldCount = targetPlanet.defense[DefenseType.PlanetaryShield] || 0
const defensePower = calculatePlanetDefensePower(targetPlanet.fleet, targetPlanet.defense)
const destructionChance = calculateDestructionChance(deathstarCount, planetaryShieldCount, defensePower)
return {
success,
destructionChance,
planetId: success ? targetPlanet.id : undefined
const randomValue = Math.random() * 100
const success = randomValue < destructionChance
mission.status = 'returning'
return {
success,
destructionChance,
planetId: success ? targetPlanet.id : undefined,
deathstarsLost: false,
deathstarDestructionChance: 0,
isMoon: false,
failReason: success ? undefined : 'chanceFailed'
}
}
}
@@ -985,17 +1132,17 @@ export const updateFleetMissions = async (
}
case MissionType.Colonize:
const newColony = processColonizeArrival(mission, targetPlanet, attacker)
if (newColony) {
newColonies.push(newColony)
planets.set(targetKey, newColony)
const colonizeResult = processColonizeArrival(mission, targetPlanet, attacker)
if (colonizeResult.success && colonizeResult.planet) {
newColonies.push(colonizeResult.planet)
planets.set(targetKey, colonizeResult.planet)
}
break
case MissionType.Spy:
const spyReport = processSpyArrival(mission, targetPlanet, attacker, defender)
if (spyReport) {
spyReports.push(spyReport)
const spyResult = processSpyArrival(mission, targetPlanet, attacker, defender)
if (spyResult.success && spyResult.report) {
spyReports.push(spyResult.report)
}
break
@@ -1010,7 +1157,7 @@ export const updateFleetMissions = async (
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
const debrisField = debrisFields.get(debrisId)
const recycleResult = processRecycleArrival(mission, debrisField, attacker, allNpcs)
if (recycleResult) {
if (recycleResult.success && recycleResult.collectedResources) {
if (recycleResult.remainingDebris) {
// 更新残骸场
const updatedDebris: DebrisField = {
@@ -1029,7 +1176,7 @@ export const updateFleetMissions = async (
case MissionType.Destroy:
const destroyResult = processDestroyArrival(mission, targetPlanet, attacker)
if (destroyResult && destroyResult.success && destroyResult.planetId) {
if (destroyResult.success && destroyResult.planetId) {
// 星球被摧毁
destroyedPlanetIds.push(destroyResult.planetId)

View File

@@ -6,6 +6,7 @@ import * as researchLogic from './researchLogic'
import * as pointsLogic from './pointsLogic'
import * as planetLogic from './planetLogic'
import * as resourceLogic from './resourceLogic'
import * as achievementLogic from './achievementLogic'
/**
* 初始化玩家数据
@@ -105,6 +106,14 @@ export const processGameUpdate = (
): {
updatedResearchQueue: BuildQueueItem[]
} => {
// 确保成就统计数据存在
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
if (!player.achievements) {
player.achievements = achievementLogic.initializeAchievements()
}
// 获取军官加成
const bonuses = officerLogic.calculateActiveBonuses(player.officers, now)
@@ -113,16 +122,48 @@ export const processGameUpdate = (
pointsLogic.addPoints(player, points)
}
// 通知回调
const onCompleted = (type: string, itemType: string, level?: number, _quantity?: number) => {
// 通知回调 + 成就统计更新
const onCompleted = (type: string, itemType: string, level?: number, quantity?: number) => {
if (onNotification) {
onNotification(type, itemType, level)
}
// 更新成就统计
if (player.achievementStats) {
if (type === 'building' && level !== undefined) {
achievementLogic.updateBuildingStats(player.achievementStats, itemType as any, level)
} else if (type === 'technology' && level !== undefined) {
achievementLogic.updateResearchStats(player.achievementStats, itemType as any, level)
} else if (type === 'ship' && quantity !== undefined) {
achievementLogic.updateShipProductionStats(player.achievementStats, itemType as any, quantity)
} else if (type === 'defense' && quantity !== undefined) {
achievementLogic.updateDefenseProductionStats(player.achievementStats, itemType as any, quantity)
}
}
}
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
player.planets.forEach(planet => {
// 计算更新前的资源(用于计算生产量)
const resourcesBefore = { ...planet.resources }
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed)
// 追踪资源生产统计
if (player.achievementStats) {
const metalProduced = Math.max(0, planet.resources.metal - resourcesBefore.metal)
const crystalProduced = Math.max(0, planet.resources.crystal - resourcesBefore.crystal)
const deuteriumProduced = Math.max(0, planet.resources.deuterium - resourcesBefore.deuterium)
const darkMatterProduced = Math.max(0, planet.resources.darkMatter - resourcesBefore.darkMatter)
if (metalProduced > 0 || crystalProduced > 0 || deuteriumProduced > 0 || darkMatterProduced > 0) {
achievementLogic.updateResourceProductionStats(player.achievementStats, {
metal: metalProduced,
crystal: crystalProduced,
deuterium: deuteriumProduced,
darkMatter: darkMatterProduced
})
}
}
})
// 更新所有星球其他状态
@@ -159,3 +200,161 @@ export const processGameUpdate = (
export const checkOfficersExpiration = (officers: Record<OfficerType, Officer>, now: number): void => {
officerLogic.checkAndDeactivateExpiredOfficers(officers, now)
}
/**
* 检查成就进度并解锁新成就
*/
export const checkAndUnlockAchievements = (player: Player): achievementLogic.AchievementUnlock[] => {
if (!player.achievementStats || !player.achievements) {
return []
}
const unlocks = achievementLogic.checkAchievements(player)
// 应用奖励
unlocks.forEach(unlock => {
achievementLogic.applyAchievementReward(player, unlock.reward)
})
return unlocks
}
/**
* 更新资源消耗统计
*/
export const trackResourceConsumption = (
player: Player,
consumed: { metal?: number; crystal?: number; deuterium?: number; darkMatter?: number }
): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
achievementLogic.updateResourceConsumptionStats(player.achievementStats, consumed)
}
/**
* 更新攻击统计(玩家作为攻击者)
*/
export const trackAttackStats = (player: Player, battleResult: any, won: boolean, debrisValue: number): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
achievementLogic.updateAttackStats(player.achievementStats, battleResult, won, debrisValue)
}
/**
* 更新防御统计(玩家作为防御者)
*/
export const trackDefenseStats = (player: Player, battleResult: any, won: boolean, debrisValue: number): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
achievementLogic.updateDefenseStats(player.achievementStats, battleResult, won, debrisValue)
}
/**
* 更新任务统计
*/
export const trackMissionStats = (
player: Player,
missionType: string,
details?: {
resourcesAmount?: number
successful?: boolean
fuelAmount?: number
}
): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
const stats = player.achievementStats
achievementLogic.updateFlightMissionStats(stats)
switch (missionType) {
case 'transport':
if (details?.resourcesAmount) {
achievementLogic.updateTransportStats(stats, details.resourcesAmount)
}
break
case 'colonize':
achievementLogic.updateColonizationStats(stats)
break
case 'spy':
achievementLogic.updateSpyStats(stats)
break
case 'deploy':
achievementLogic.updateDeploymentStats(stats)
break
case 'expedition':
achievementLogic.updateExpeditionStats(stats, details?.successful ?? false)
break
case 'recycle':
// 回收任务总是计入任务次数,但只有有资源时才增加回收资源量
achievementLogic.updateRecyclingStats(stats, details?.resourcesAmount ?? 0)
break
case 'destroy':
achievementLogic.updatePlanetDestructionStats(stats)
break
}
if (details?.fuelAmount) {
achievementLogic.updateFuelConsumptionStats(stats, details.fuelAmount)
}
}
/**
* 更新外交统计
*/
export const trackDiplomacyStats = (
player: Player,
eventType: string,
details?: {
resourcesAmount?: number
friendlyCount?: number
hostileCount?: number
}
): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
const stats = player.achievementStats
switch (eventType) {
case 'gift':
if (details?.resourcesAmount) {
achievementLogic.updateGiftStats(stats, details.resourcesAmount)
}
break
case 'attackedByNPC':
achievementLogic.updateAttackedByNPCStats(stats)
break
case 'spiedByNPC':
achievementLogic.updateSpiedByNPCStats(stats)
break
case 'debrisRecycledByNPC':
if (details?.resourcesAmount) {
achievementLogic.updateDebrisRecycledByNPCStats(stats, details.resourcesAmount)
}
break
case 'updateRelations':
if (details?.friendlyCount !== undefined) {
achievementLogic.updateFriendlyNPCStats(stats, details.friendlyCount)
}
if (details?.hostileCount !== undefined) {
achievementLogic.updateHostileNPCStats(stats, details.hostileCount)
}
break
}
}
/**
* 追踪燃料消耗(在舰队出发时调用)
*/
export const trackFuelConsumption = (player: Player, fuelAmount: number): void => {
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
achievementLogic.updateFuelConsumptionStats(player.achievementStats, fuelAmount)
}

View File

@@ -4,8 +4,9 @@ import { MOON_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig'
/**
* 计算月球生成概率
* OGame规则每100,000残骸(金属+晶体) = 1%概率最高20%需要2,000,000残骸
* @param debrisField 战斗产生的残骸场
* @returns 生成概率0-100
* @returns 生成概率0-20
*/
export const calculateMoonGenerationChance = (debrisField: Resources): number => {
const totalDebris = debrisField.metal + debrisField.crystal
@@ -15,18 +16,42 @@ export const calculateMoonGenerationChance = (debrisField: Resources): number =>
return 0
}
// 计算概率:基础概率 + (残骸量 / 每单位增加量) * 1%
const additionalChance = Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)
const chance = MOON_CONFIG.baseChance + additionalChance
// 计算概率:每100k残骸 = 1%
const chance = Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)
// 限制在最大概率内
// 限制在最大概率内20%
return Math.min(chance, MOON_CONFIG.maxChance)
}
/**
* 计算月球直径
* OGame规则直径基于生成时的moonchance20%概率保证直径>8000km
* @param moonChance 月球生成概率1-20
* @returns 月球直径(km)
*/
export const calculateMoonDiameter = (moonChance: number): number => {
// 基础直径 + 每%概率增加的直径 + 随机波动
const baseDiameter = MOON_CONFIG.baseDiameter
const bonusDiameter = moonChance * MOON_CONFIG.diameterPerChance
// 添加±10%的随机波动
const randomFactor = 0.9 + Math.random() * 0.2
const diameter = Math.floor((baseDiameter + bonusDiameter) * randomFactor)
// 确保在合理范围内
return Math.max(MOON_CONFIG.minDiameter, Math.min(MOON_CONFIG.maxDiameter, diameter))
}
/**
* 尝试生成月球
* OGame规则
* - 每100k残骸 = 1%概率
* - 最高20%概率需要2M残骸
* - 月球初始只有1格空间
* - 直径基于moonchance计算
*
* @param debrisField 战斗产生的残骸场
* @param planetPosition 星球坐标
* @param planetId 母星ID
* @param playerId 玩家ID
* @returns 生成的月球对象如果未生成则返回null
*/
@@ -49,6 +74,9 @@ export const tryGenerateMoon = (
return null
}
// 计算月球直径
const diameter = calculateMoonDiameter(chance)
// 生成月球
const moon: Planet = {
id: `moon_${Date.now()}`,
@@ -89,10 +117,11 @@ export const tryGenerateMoon = (
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize,
maxSpace: MOON_CONFIG.baseFields, // OGame规则月球初始只有1格空间
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true,
parentPlanetId: planetId
parentPlanetId: planetId,
diameter: diameter // 月球直径(km)
}
// 初始化所有建筑为0级
@@ -118,3 +147,182 @@ export const hasMoonAtPosition = (planets: Planet[], position: { galaxy: number;
p.position.position === position.position
)
}
/**
* 月球销毁结果类型
*/
export interface MoonDestructionResult {
moonDestroyed: boolean // 月球是否被销毁
deathstarsDestroyed: boolean // 死星是否被反向销毁
moonDestructionChance: number // 月球销毁概率
deathstarDestructionChance: number // 死星被销毁概率
}
/**
* 计算月球销毁概率
* OGame规则销毁概率 = (100 - √diameter) × √deathstars
* @param moonDiameter 月球直径(km)
* @param deathstarCount 死星数量
* @returns 销毁概率(0-100)
*/
export const calculateMoonDestructionChance = (moonDiameter: number, deathstarCount: number): number => {
if (deathstarCount <= 0) return 0
// 公式:(100 - √diameter) × √deathstars
const chance = (100 - Math.sqrt(moonDiameter)) * Math.sqrt(deathstarCount)
// 限制在0-100之间
return Math.max(0, Math.min(100, chance))
}
/**
* 计算死星被反向销毁的概率
* OGame规则反向销毁概率 = √diameter / 2
* @param moonDiameter 月球直径(km)
* @returns 死星被销毁概率(0-100)
*/
export const calculateDeathstarDestructionChance = (moonDiameter: number): number => {
// 公式√diameter / 2
const chance = Math.sqrt(moonDiameter) / 2
// 限制在0-100之间
return Math.max(0, Math.min(100, chance))
}
/**
* 尝试销毁月球(死星攻击月球时调用)
* OGame规则
* - 死星攻击月球时,先进行正常战斗
* - 战斗后,如果死星存活,则判定月球销毁
* - 月球销毁概率 = (100 - √diameter) × √deathstars
* - 死星反向销毁概率 = √diameter / 2
* - 两个概率独立判定
*
* @param moon 目标月球
* @param deathstarCount 攻击的死星数量
* @returns 销毁结果
*/
export const tryDestroyMoon = (moon: Planet, deathstarCount: number): MoonDestructionResult => {
if (!moon.isMoon || deathstarCount <= 0) {
return {
moonDestroyed: false,
deathstarsDestroyed: false,
moonDestructionChance: 0,
deathstarDestructionChance: 0
}
}
const diameter = moon.diameter || MOON_CONFIG.minDiameter
// 计算概率
const moonDestructionChance = calculateMoonDestructionChance(diameter, deathstarCount)
const deathstarDestructionChance = calculateDeathstarDestructionChance(diameter)
// 独立判定
const moonRoll = Math.random() * 100
const deathstarRoll = Math.random() * 100
const moonDestroyed = moonRoll < moonDestructionChance
const deathstarsDestroyed = deathstarRoll < deathstarDestructionChance
return {
moonDestroyed,
deathstarsDestroyed,
moonDestructionChance,
deathstarDestructionChance
}
}
/**
* 计算传感器阵列扫描范围
* OGame规则范围 = level² - 1 个星系
* @param sensorPhalanxLevel 传感器阵列等级
* @returns 可扫描的星系范围(单向)
*/
export const calculateSensorPhalanxRange = (sensorPhalanxLevel: number): number => {
if (sensorPhalanxLevel <= 0) return 0
return sensorPhalanxLevel * sensorPhalanxLevel - 1
}
/**
* 检查目标坐标是否在传感器阵列扫描范围内
* @param moonPosition 月球坐标
* @param targetPosition 目标坐标
* @param sensorPhalanxLevel 传感器阵列等级
* @returns 是否在扫描范围内
*/
export const isInSensorPhalanxRange = (
moonPosition: { galaxy: number; system: number; position: number },
targetPosition: { galaxy: number; system: number; position: number },
sensorPhalanxLevel: number
): boolean => {
// 必须在同一银河系
if (moonPosition.galaxy !== targetPosition.galaxy) return false
const range = calculateSensorPhalanxRange(sensorPhalanxLevel)
const systemDistance = Math.abs(moonPosition.system - targetPosition.system)
return systemDistance <= range
}
/**
* 检查跳跃门是否可用(冷却是否结束)
* OGame规则跳跃门使用后有1小时冷却时间
* @param moon 月球对象
* @returns 是否可以使用跳跃门
*/
export const isJumpGateReady = (moon: Planet): boolean => {
if (!moon.isMoon) return false
if (!moon.buildings[BuildingType.JumpGate] || moon.buildings[BuildingType.JumpGate] <= 0) return false
const lastUsed = moon.jumpGateLastUsed || 0
const cooldownEnd = lastUsed + MOON_CONFIG.jumpGateCooldown
return Date.now() >= cooldownEnd
}
/**
* 获取跳跃门剩余冷却时间(毫秒)
* @param moon 月球对象
* @returns 剩余冷却时间毫秒0表示已冷却完成
*/
export const getJumpGateCooldownRemaining = (moon: Planet): number => {
if (!moon.isMoon) return 0
if (!moon.buildings[BuildingType.JumpGate] || moon.buildings[BuildingType.JumpGate] <= 0) return 0
const lastUsed = moon.jumpGateLastUsed || 0
const cooldownEnd = lastUsed + MOON_CONFIG.jumpGateCooldown
const remaining = cooldownEnd - Date.now()
return Math.max(0, remaining)
}
/**
* 使用跳跃门(记录使用时间)
* @param moon 月球对象
*/
export const useJumpGate = (moon: Planet): void => {
if (moon.isMoon) {
moon.jumpGateLastUsed = Date.now()
}
}
/**
* 检查两个月球之间是否可以使用跳跃门传送
* OGame规则两个月球都必须有跳跃门且都必须冷却完成
* @param sourceMoon 源月球
* @param targetMoon 目标月球
* @returns 是否可以传送
*/
export const canUseJumpGate = (sourceMoon: Planet, targetMoon: Planet): boolean => {
// 两个都必须是月球
if (!sourceMoon.isMoon || !targetMoon.isMoon) return false
// 两个都必须有跳跃门
const sourceHasGate = sourceMoon.buildings[BuildingType.JumpGate] > 0
const targetHasGate = targetMoon.buildings[BuildingType.JumpGate] > 0
if (!sourceHasGate || !targetHasGate) return false
// 两个跳跃门都必须冷却完成
return isJumpGateReady(sourceMoon) && isJumpGateReady(targetMoon)
}

View File

@@ -1,5 +1,5 @@
import type { NPC, Planet, Player } from '@/types/game'
import { TechnologyType, BuildingType, ShipType } from '@/types/game'
import { TechnologyType, BuildingType, ShipType, DefenseType } from '@/types/game'
import { BUILDINGS, SHIPS, TECHNOLOGIES } from '@/config/gameConfig'
import * as buildingLogic from './buildingLogic'
import * as researchLogic from './researchLogic'
@@ -632,3 +632,347 @@ export const initializeNPCDiplomacy = (npcs: NPC[]): void => {
}
})
}
// ==================== 距离难度系统 ====================
/**
* 计算NPC星球到玩家母星的距离
* 银河系距离权重最大,星系次之,位置最小
*/
export const calculateDistanceToHomeworld = (
npcPosition: { galaxy: number; system: number; position: number },
homeworldPosition: { galaxy: number; system: number; position: number }
): number => {
const galaxyDistance = Math.abs(npcPosition.galaxy - homeworldPosition.galaxy)
const systemDistance = Math.abs(npcPosition.system - homeworldPosition.system)
const positionDistance = Math.abs(npcPosition.position - homeworldPosition.position)
// 银河系跨度权重最大
return galaxyDistance * 100 + systemDistance * 10 + positionDistance
}
/**
* 基于距离计算难度等级1-无限)
*/
export const calculateDifficultyLevel = (distance: number): number => {
if (distance <= 10) return 1 // 新手
if (distance <= 30) return 2 // 简单
if (distance <= 60) return 3 // 普通
if (distance <= 100) return 4 // 困难
if (distance <= 200) return 5 // 专家
if (distance <= 400) return 6 // 大师
// 超过400继续增长
return 6 + Math.floor((distance - 400) / 200)
}
/**
* 距离难度倍率配置
*/
export interface DistanceDifficultyMultipliers {
buildingMultiplier: number
techMultiplier: number
fleetMultiplier: number
resourceMultiplier: number
defenseMultiplier: number
}
/**
* 基于距离计算实力倍率
* 使用对数增长,确保数值合理
*
* 建筑等级上限30
* 科技等级上限20
*
* 建筑等级示例baseLevel=5
* - 距离 0: 5级
* - 距离 10: 8级
* - 距离 50: 14级
* - 距离 100: 18级
* - 距离 200: 22级
* - 距离 400: 26级
* - 距离 800+: 30级上限
*/
export const calculateDistanceDifficultyMultiplier = (distance: number): DistanceDifficultyMultipliers => {
// 使用对数增长,确保建筑等级在合理范围内
// 公式: 1 + ln(1 + distance) * 0.8
// 这样可以确保最大建筑倍率约为6倍5*6=30
const logMultiplier = 1 + Math.log(1 + distance) * 0.8
// 限制建筑倍率最大为6因为基础等级是55*6=30
const buildingMultiplier = Math.min(6, Math.max(1, Math.round(logMultiplier)))
// 科技倍率稍低上限4倍5*4=20
const techMultiplier = Math.min(4, Math.max(1, Math.round(logMultiplier * 0.7)))
// 舰队倍率:使用更平缓的增长
// 基础舰队10艘最大约500艘左右
const fleetMultiplier = Math.min(50, Math.max(1, Math.round(logMultiplier * 8)))
// 资源倍率限制最大20倍避免Infinity
const resourceMultiplier = Math.min(20, Math.max(1, logMultiplier * 3))
// 防御倍率
const defenseMultiplier = Math.min(30, Math.max(1, Math.round(logMultiplier * 5)))
return {
buildingMultiplier,
techMultiplier,
fleetMultiplier,
resourceMultiplier,
defenseMultiplier
}
}
/**
* 基于距离难度初始化NPC星球
* 替代旧的 initializeNPCStartingPower
*
* 建筑等级上限30
* 科技等级上限20
* 资源上限:基于仓储建筑等级计算 (10000 * 2^level)
* 舰队数量:基于船坞等级和难度等级合理计算
*/
export const initializeNPCByDistance = (
npc: NPC,
homeworldPosition: { galaxy: number; system: number; position: number }
): void => {
const planet = npc.planets[0]
if (!planet) return
const distance = calculateDistanceToHomeworld(planet.position, homeworldPosition)
const multipliers = calculateDistanceDifficultyMultiplier(distance)
// 保存距离和难度等级到NPC
npc.distanceToHomeworld = distance
npc.difficultyLevel = calculateDifficultyLevel(distance)
// 基础等级 * 倍率,并限制上限
const baseLevel = 5
const MAX_BUILDING_LEVEL = 30
const MAX_TECH_LEVEL = 20
const targetBuildingLevel = Math.min(MAX_BUILDING_LEVEL, Math.max(1, Math.floor(baseLevel * multipliers.buildingMultiplier)))
const targetTechLevel = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(baseLevel * multipliers.techMultiplier)))
// 设置资源建筑上限30
planet.buildings[BuildingType.MetalMine] = Math.min(MAX_BUILDING_LEVEL, targetBuildingLevel)
planet.buildings[BuildingType.CrystalMine] = Math.min(MAX_BUILDING_LEVEL, Math.max(1, Math.floor(targetBuildingLevel * 0.8)))
planet.buildings[BuildingType.DeuteriumSynthesizer] = Math.min(MAX_BUILDING_LEVEL, Math.max(1, Math.floor(targetBuildingLevel * 0.6)))
planet.buildings[BuildingType.SolarPlant] = Math.min(MAX_BUILDING_LEVEL, targetBuildingLevel + 2)
// 设置设施建筑
planet.buildings[BuildingType.RoboticsFactory] = Math.min(15, Math.max(1, Math.floor(targetBuildingLevel * 0.5)))
planet.buildings[BuildingType.Shipyard] = Math.min(12, Math.max(1, Math.floor(targetBuildingLevel * 0.4)))
planet.buildings[BuildingType.ResearchLab] = Math.min(12, Math.max(1, Math.floor(targetBuildingLevel * 0.4)))
// 设置仓储上限10级对应10000*2^10=10,240,000容量
const storageLevel = Math.min(10, Math.max(1, Math.floor(targetBuildingLevel * 0.3)))
planet.buildings[BuildingType.MetalStorage] = storageLevel
planet.buildings[BuildingType.CrystalStorage] = storageLevel
planet.buildings[BuildingType.DeuteriumTank] = storageLevel
// 设置科技上限20
npc.technologies[TechnologyType.EnergyTechnology] = Math.min(MAX_TECH_LEVEL, targetTechLevel)
npc.technologies[TechnologyType.ComputerTechnology] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.8)))
npc.technologies[TechnologyType.WeaponsTechnology] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.7)))
npc.technologies[TechnologyType.ShieldingTechnology] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.7)))
npc.technologies[TechnologyType.ArmourTechnology] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.7)))
npc.technologies[TechnologyType.CombustionDrive] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.6)))
npc.technologies[TechnologyType.EspionageTechnology] = Math.min(MAX_TECH_LEVEL, Math.max(1, Math.floor(targetTechLevel * 0.5)))
// 计算舰队仓储容量(船坞每级+1000
const shipyardLevel = planet.buildings[BuildingType.Shipyard] || 1
const fleetStorageCapacity = shipyardLevel * 1000
// 基于难度等级和船坞容量计算舰队数量
// 难度等级1-7对应不同的舰队规模
const difficultyLevel = npc.difficultyLevel || 1
const fleetScale = Math.min(1, difficultyLevel / 7) // 0.14 ~ 1.0
// 设置舰队(基于船坞容量和难度等级)
// 总舰队数量不超过船坞容量的80%
const maxTotalFleet = Math.floor(fleetStorageCapacity * 0.8)
const baseFleetCount = Math.floor(maxTotalFleet * fleetScale)
// 分配舰队比例
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.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)
// 设置防御设施(基于难度等级,合理范围)
const defenseScale = difficultyLevel * 5
planet.defense[DefenseType.RocketLauncher] = Math.floor(defenseScale * 2)
planet.defense[DefenseType.LightLaser] = Math.floor(defenseScale * 1.5)
planet.defense[DefenseType.HeavyLaser] = Math.floor(defenseScale * 0.8)
planet.defense[DefenseType.GaussCannon] = Math.floor(defenseScale * 0.3)
planet.defense[DefenseType.IonCannon] = Math.floor(defenseScale * 0.3)
// 高级防御设施只有高难度NPC才有
if (difficultyLevel >= 4) {
planet.defense[DefenseType.PlasmaTurret] = Math.floor(defenseScale * 0.1)
planet.defense[DefenseType.SmallShieldDome] = 1
}
if (difficultyLevel >= 6) {
planet.defense[DefenseType.LargeShieldDome] = 1
}
// 设置资源(基于仓储建筑等级计算容量上限)
// 容量公式10000 * 2^level
const metalCapacity = 10000 * Math.pow(2, planet.buildings[BuildingType.MetalStorage] || 0)
const crystalCapacity = 10000 * Math.pow(2, planet.buildings[BuildingType.CrystalStorage] || 0)
const deuteriumCapacity = 10000 * Math.pow(2, planet.buildings[BuildingType.DeuteriumTank] || 0)
const darkMatterCapacity = 1000 * Math.pow(2, planet.buildings[BuildingType.DarkMatterTank] || 0)
// 资源设置为容量的50%-80%(基于难度等级)
const resourceFillRate = 0.5 + (difficultyLevel / 7) * 0.3
planet.resources.metal = Math.floor(metalCapacity * resourceFillRate)
planet.resources.crystal = Math.floor(crystalCapacity * resourceFillRate)
planet.resources.deuterium = Math.floor(deuteriumCapacity * resourceFillRate)
planet.resources.darkMatter = Math.floor(darkMatterCapacity * resourceFillRate)
}
/**
* 基于距离的NPC成长配置
*/
export interface DistanceBasedGrowthConfig {
resourceGrowthRate: number
buildingGrowthSpeed: number
techGrowthSpeed: number
checkInterval: number
}
/**
* 根据距离计算NPC成长配置
*/
export const calculateDistanceBasedGrowthConfig = (distance: number): DistanceBasedGrowthConfig => {
const multipliers = calculateDistanceDifficultyMultiplier(distance)
return {
resourceGrowthRate: multipliers.resourceMultiplier,
buildingGrowthSpeed: multipliers.buildingMultiplier,
techGrowthSpeed: multipliers.techMultiplier,
checkInterval: 180 // 3分钟检查一次
}
}
/**
* 基于距离更新NPC成长
* 替代旧的 updateNPCGrowth 中基于玩家积分的逻辑
*/
export const updateNPCGrowthByDistance = (
npc: NPC,
homeworldPosition: { galaxy: number; system: number; position: number },
deltaSeconds: number,
gameSpeed: number = 1
): void => {
const planet = npc.planets[0]
if (!planet) return
// 如果没有距离信息,先计算
if (npc.distanceToHomeworld === undefined) {
npc.distanceToHomeworld = calculateDistanceToHomeworld(planet.position, homeworldPosition)
npc.difficultyLevel = calculateDifficultyLevel(npc.distanceToHomeworld)
}
const config = calculateDistanceBasedGrowthConfig(npc.distanceToHomeworld)
// 1. 持续生成资源(应用游戏速度倍率)
generateNPCResourcesByDistance(npc, deltaSeconds, config, gameSpeed)
// 2. 定期评估并调整实力
const now = Date.now()
const lastGrowthCheck = (npc as any).lastGrowthCheck || 0
if (now - lastGrowthCheck >= config.checkInterval * 1000) {
;(npc as any).lastGrowthCheck = now
// 计算目标实力(基于距离倍率)
const multipliers = calculateDistanceDifficultyMultiplier(npc.distanceToHomeworld)
const baseLevel = 5
const targetBuildingLevel = Math.floor(baseLevel * multipliers.buildingMultiplier)
const targetTechLevel = Math.floor(baseLevel * multipliers.techMultiplier)
// 获取当前平均建筑等级
let totalBuildingLevels = 0
let buildingCount = 0
Object.values(planet.buildings).forEach(level => {
totalBuildingLevels += level
buildingCount++
})
const avgBuildingLevel = buildingCount > 0 ? totalBuildingLevels / buildingCount : 0
// 获取当前平均科技等级
const techLevels = Object.values(npc.technologies)
const avgTechLevel = techLevels.length > 0 ? techLevels.reduce((sum, level) => sum + level, 0) / techLevels.length : 0
// 如果实力不足,进行升级
if (avgBuildingLevel < targetBuildingLevel) {
autoUpgradeNPCBuildings(npc)
}
if (avgTechLevel < targetTechLevel) {
autoResearchNPCTechnologies(npc)
}
// 计算目标舰队战力
const targetFleetPower = 1000 * multipliers.fleetMultiplier
let currentFleetPower = 0
Object.entries(planet.fleet).forEach(([shipType, count]) => {
const shipConfig = SHIPS[shipType as ShipType]
const power = shipConfig.attack + shipConfig.shield + shipConfig.armor / 10
currentFleetPower += power * (count as number)
})
if (currentFleetPower < targetFleetPower) {
autoBuildNPCFleet(npc)
}
}
}
/**
* 基于距离生成NPC资源
*/
export const generateNPCResourcesByDistance = (
npc: NPC,
deltaSeconds: number,
config: DistanceBasedGrowthConfig,
gameSpeed: number = 1
): void => {
const planet = npc.planets[0]
if (!planet) return
// 基于建筑等级计算资源产量
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
const deuteriumLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
const darkMatterLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
// 简化的资源产量计算(每秒产量)
const metalProduction = 30 * metalMineLevel * Math.pow(1.1, metalMineLevel) * config.resourceGrowthRate
const crystalProduction = 20 * crystalMineLevel * Math.pow(1.1, crystalMineLevel) * config.resourceGrowthRate
const deuteriumProduction = 10 * deuteriumLevel * Math.pow(1.1, deuteriumLevel) * config.resourceGrowthRate
const darkMatterProduction = ((25 * darkMatterLevel * Math.pow(1.5, darkMatterLevel)) / 3600) * config.resourceGrowthRate
// 应用游戏速度倍率到时间
const effectiveDeltaSeconds = deltaSeconds * gameSpeed
// 增加资源
planet.resources.metal += metalProduction * effectiveDeltaSeconds
planet.resources.crystal += crystalProduction * effectiveDeltaSeconds
planet.resources.deuterium += deuteriumProduction * effectiveDeltaSeconds
planet.resources.darkMatter += darkMatterProduction * effectiveDeltaSeconds
// 确保不超过存储上限
const metalStorage = planet.buildings[BuildingType.MetalStorage] || 0
const crystalStorage = planet.buildings[BuildingType.CrystalStorage] || 0
const deuteriumStorage = planet.buildings[BuildingType.DeuteriumTank] || 0
const darkMatterStorage = planet.buildings[BuildingType.DarkMatterTank] || 0
planet.resources.metal = Math.min(planet.resources.metal, 10000 * Math.pow(2, metalStorage))
planet.resources.crystal = Math.min(planet.resources.crystal, 10000 * Math.pow(2, crystalStorage))
planet.resources.deuterium = Math.min(planet.resources.deuterium, 10000 * Math.pow(2, deuteriumStorage))
planet.resources.darkMatter = Math.min(planet.resources.darkMatter, 1000 * Math.pow(2, darkMatterStorage))
}

View File

@@ -143,12 +143,18 @@ export const calculateMoonChance = (debrisField: Resources): number => {
/**
* 创建月球
* @param parentPlanet 母星球
* @param position 坐标
* @param playerId 玩家ID
* @param moonSuffix 月球名称后缀
* @param diameter 月球直径(km),用于计算销毁概率
*/
export const createMoon = (
parentPlanet: Planet,
position: { galaxy: number; system: number; position: number },
playerId: string,
moonSuffix: string = "'s Moon"
moonSuffix: string = "'s Moon",
diameter?: number
): Planet => {
const moonId = `moon_${Date.now()}`
const moon: Planet = {
@@ -196,10 +202,11 @@ export const createMoon = (
},
buildQueue: [],
lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize,
maxSpace: MOON_CONFIG.baseFields, // OGame规则月球初始只有1格空间
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true,
parentPlanetId: parentPlanet.id
parentPlanetId: parentPlanet.id,
diameter: diameter || MOON_CONFIG.minDiameter // 月球直径(km)
}
// 初始化建筑等级
@@ -212,11 +219,12 @@ export const createMoon = (
/**
* 计算月球空间上限
* OGame规则月球初始1格月球基地每级+3格但月球基地本身占用1格净增2格
*/
export const calculateMoonMaxSpace = (moon: Planet): number => {
if (!moon.isMoon) return 0
const lunarBaseLevel = moon.buildings[BuildingType.LunarBase] || 0
return MOON_CONFIG.baseSize + lunarBaseLevel * MOON_CONFIG.lunarBaseSpaceBonus
return MOON_CONFIG.baseFields + lunarBaseLevel * MOON_CONFIG.lunarBaseFieldsBonus
}
/**

View File

@@ -120,31 +120,31 @@ export const getResourceCapacity = (planet: Planet, officers: Record<OfficerType
* 计算最大建造队列数量
* @param planet 星球对象
* @param additionalBuildQueue 军官提供的额外队列数量
* @returns 最大建造队列数量(基础1个 + 纳米工厂等级 + 军官加成最多10个
* @returns 最大建造队列数量(基础3个 + 纳米工厂等级 + 军官加成最多10个
*/
export const getMaxBuildQueue = (planet: Planet, additionalBuildQueue: number = 0): number => {
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
return Math.min(1 + naniteFactoryLevel + additionalBuildQueue, 10)
return Math.min(3 + naniteFactoryLevel + additionalBuildQueue, 10)
}
/**
* 计算最大研究队列数量
* @param technologies 已研究的科技等级
* @returns 最大研究队列数量(基础1个 + 计算机技术等级最多10个
* @returns 最大研究队列数量(基础3个 + 计算机技术等级最多10个
*/
export const getMaxResearchQueue = (technologies: Partial<Record<TechnologyType, number>>): number => {
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
return Math.min(1 + computerTechLevel, 10)
return Math.min(3 + computerTechLevel, 10)
}
/**
* 计算最大舰队任务数量
* @param additionalFleetSlots 军官提供的额外槽位数量
* @param computerTechnologyLevel 计算机技术等级
* @returns 最大舰队任务数量(基础1个 + 计算机技术等级 + 军官加成最多20个
* @returns 最大舰队任务数量(基础3个 + 计算机技术等级 + 军官加成最多20个
*/
export const getMaxFleetMissions = (additionalFleetSlots: number = 0, computerTechnologyLevel: number = 0): number => {
return Math.min(1 + computerTechnologyLevel + additionalFleetSlots, 20)
return Math.min(3 + computerTechnologyLevel + additionalFleetSlots, 20)
}
/**

View File

@@ -97,6 +97,8 @@ export const calculateResourceCapacity = (planet: Planet, storageCapacityBonus:
const bonus = 1 + (storageCapacityBonus || 0) / 100
// OGame规则基础容量10000资源可以超过容量只影响生产不会丢失
// 月球没有矿场,所以超过容量没有影响,玩家可以从行星运输资源到月球
const baseCapacity = 10000
const darkMatterBaseCapacity = 1000 // 暗物质基础容量较小
return {

View File

@@ -16,6 +16,7 @@ const router = createRouter({
{ path: '/messages', name: 'messages', component: () => import('@/views/MessagesView.vue') },
{ 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: '/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') }

View File

@@ -10,9 +10,12 @@ import type {
SpiedNotification,
NPCActivityNotification,
IncomingFleetAlert,
MissileAttack
MissileAttack,
AchievementStats,
AchievementProgress
} from '@/types/game'
import { TechnologyType, OfficerType } from '@/types/game'
import { initializeAchievementStats, initializeAchievements } from '@/logic/achievementLogic'
import type { Locale } from '@/locales'
import pkg from '../../package.json'
import { encryptData, decryptData } from '@/utils/crypto'
@@ -41,7 +44,9 @@ export const useGameStore = defineStore('game', {
giftRejectedNotifications: [],
points: 0,
isGMEnabled: false, // 明确设置 GM 模式默认为 false
lastVersionCheckTime: 0 // 最后一次检查版本的时间戳默认为0
lastVersionCheckTime: 0, // 最后一次检查版本的时间戳默认为0
achievementStats: initializeAchievementStats() as AchievementStats,
achievements: initializeAchievements() as Record<string, AchievementProgress>
} as Player,
currentPlanetId: '',
isDark: '',

View File

@@ -267,6 +267,16 @@ export interface DiplomaticReport {
read?: boolean // 已读状态
}
// 舰队预设
export interface FleetPreset {
id: string
name: string // 自定义预设名称
fleet: Partial<Fleet> // 预设舰队数量
targetPosition?: { galaxy: number; system: number; position: number } // 预设目标坐标
missionType?: MissionType // 预设任务类型
cargo?: Partial<Resources> // 预设运输资源
}
// 舰队任务
export interface FleetMission {
id: string
@@ -275,6 +285,7 @@ export interface FleetMission {
isHostile?: boolean // 是否是敌对任务(用于警告显示)
originPlanetId: string
targetPosition: { galaxy: number; system: number; position: number }
targetIsMoon?: boolean // 目标是否为月球(用于区分同坐标的行星和月球)
targetPlanetId?: string
debrisFieldId?: string // 残骸场ID用于回收任务
missionType: MissionType
@@ -413,6 +424,8 @@ export interface MissionReport {
message: string // 任务结果描述
// 任务特定的详细信息
details?: {
// 通用:失败原因
failReason?: string
// 运输任务:运输的资源
transportedResources?: Resources
// 殖民任务:新星球信息
@@ -423,6 +436,9 @@ export interface MissionReport {
remainingDebris?: Pick<Resources, 'metal' | 'crystal'>
// 毁灭任务:摧毁的星球
destroyedPlanetName?: string
// 毁灭任务:概率和死星损失
destructionChance?: number
deathstarsLost?: boolean
// 部署任务:部署的舰队
deployedFleet?: Partial<Fleet>
// 导弹攻击任务:导弹信息
@@ -436,6 +452,8 @@ export interface MissionReport {
foundFleet?: Partial<Fleet>
// 探险任务:损失的舰船
fleetLost?: Partial<Fleet>
// 侦查任务报告ID
spyReportId?: string
}
read?: boolean
}
@@ -501,6 +519,8 @@ export interface Planet {
maxFleetStorage: number // 舰队仓储上限
isMoon: boolean // 是否为月球
parentPlanetId?: string // 如果是月球,指向母星的ID
diameter?: number // 月球直径(km),用于销毁概率计算
jumpGateLastUsed?: number // 跳跃门上次使用时间戳(ms),用于冷却计算
}
// 月球特殊配置
@@ -586,6 +606,11 @@ export interface Player {
hintsEnabled?: boolean // 是否启用弱引导提示默认true
// 显示设置
backgroundEnabled?: boolean // 是否启用背景动画默认false
// 舰队预设
fleetPresets?: FleetPreset[] // 舰队预设列表最多3个
// 成就系统
achievementStats?: AchievementStats // 成就统计数据
achievements?: Record<string, AchievementProgress> // 成就进度
}
export interface NotificationSettings {
@@ -624,7 +649,10 @@ export interface NPC {
note?: string // 玩家添加的备注
planets: Planet[]
technologies: Record<TechnologyType, number>
difficulty: 'easy' | 'medium' | 'hard'
difficulty: 'easy' | 'medium' | 'hard' // 保留兼容,不再使用
// 距离难度系统
difficultyLevel?: number // 基于距离的难度等级1-无限)
distanceToHomeworld?: number // 到玩家母星的距离
// 行为跟踪字段
lastSpyTime?: number // 上次侦查玩家的时间
lastAttackTime?: number // 上次攻击玩家的时间
@@ -676,3 +704,121 @@ export interface TutorialProgress {
currentStep: string | null // 当前步骤ID
skippedAt?: number // 跳过的时间戳
}
// ==================== 成就系统类型 ====================
// 成就类别枚举
export const AchievementCategory = {
Resource: 'resource',
Building: 'building',
Combat: 'combat',
Mission: 'mission',
Diplomacy: 'diplomacy'
} as const
export type AchievementCategory = (typeof AchievementCategory)[keyof typeof AchievementCategory]
// 成就等级枚举
export const AchievementTier = {
Bronze: 'bronze',
Silver: 'silver',
Gold: 'gold',
Platinum: 'platinum',
Diamond: 'diamond'
} as const
export type AchievementTier = (typeof AchievementTier)[keyof typeof AchievementTier]
// 成就统计数据接口
export interface AchievementStats {
// 资源统计
totalMetalProduced: number
totalCrystalProduced: number
totalDeuteriumProduced: number
totalDarkMatterProduced: number
totalMetalConsumed: number
totalCrystalConsumed: number
totalDeuteriumConsumed: number
totalDarkMatterConsumed: number
// 建造统计
buildingsUpgraded: number
maxBuildingLevel: Record<BuildingType, number>
researchCompleted: number
maxTechnologyLevel: Record<TechnologyType, number>
shipsProduced: Record<ShipType, number>
totalShipsProduced: number
defensesBuilt: Record<DefenseType, number>
totalDefensesBuilt: number
// 战斗统计
attacksLaunched: number
attacksWon: number
attacksLost: number
fleetLostInAttack: Record<ShipType, number>
totalFleetLostInAttack: number
debrisCreatedFromAttacks: number
defensesSuccessful: number
defensesFailed: number
fleetLostInDefense: Record<ShipType, number>
totalFleetLostInDefense: number
defenseLostInDefense: Record<DefenseType, number>
totalDefenseLostInDefense: number
enemyFleetDestroyedInDefense: Record<ShipType, number>
totalEnemyFleetDestroyedInDefense: number
debrisCreatedFromDefenses: number
// 任务统计
totalFlightMissions: number
transportMissions: number
transportedResources: number
colonizations: number
spyMissions: number
deployments: number
expeditionsTotal: number
expeditionsSuccessful: number
recyclingMissions: number
recycledResources: number
planetDestructions: number
fuelConsumed: number
// 外交统计
friendlyNPCCount: number
hostileNPCCount: number
giftsSent: number
giftResourcesTotal: number
attackedByNPC: number
spiedByNPC: number
debrisRecycledByNPC: number
debrisResourcesLostToNPC: number
}
// 成就等级配置
export interface AchievementTierConfig {
tier: AchievementTier
target: number
reward: AchievementReward
}
// 成就奖励
export interface AchievementReward {
darkMatter?: number
points?: number
}
// 成就配置接口
export interface AchievementConfig {
id: string
category: AchievementCategory
icon: string
tiers: AchievementTierConfig[]
statKey: keyof AchievementStats | string
checkType: 'gte' | 'eq' | 'sum' | 'max'
}
// 玩家成就进度
export interface AchievementProgress {
id: string
currentTier: AchievementTier | null
currentValue: number
unlockedAt?: number
tierUnlocks: Record<AchievementTier, number | null>
}

View File

@@ -0,0 +1,343 @@
<template>
<div class="container mx-auto p-4 sm:p-6 space-y-4 sm:space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('achievements.title') }}</h1>
<div class="flex items-center gap-2">{{ unlockedCount }} / {{ totalCount }} {{ t('achievements.unlocked') }}</div>
</div>
<!-- 分类标签 -->
<Tabs v-model="activeCategory" class="w-full">
<TabsList class="w-full grid grid-cols-5 h-10">
<TabsTrigger v-for="category in categories" :key="category.value" :value="category.value" class="text-xs sm:text-sm">
{{ t(`achievements.categories.${category.value}`) }}
<Badge v-if="getCategoryUnlockedCount(category.value) > 0" class="ml-1 h-5 px-1.5 text-[10px] bg-primary text-primary-foreground">
{{ getCategoryUnlockedCount(category.value) }}
</Badge>
</TabsTrigger>
</TabsList>
<!-- 成就卡片网格 -->
<TabsContent v-for="category in categories" :key="category.value" :value="category.value" class="mt-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card v-for="achievement in getAchievementsByCategory(category.value)" :key="achievement.id" class="relative overflow-hidden">
<!-- 等级指示条 -->
<div class="absolute top-0 left-0 right-0 h-1 flex">
<div v-for="tier in tierOrder" :key="tier" class="flex-1" :class="getTierBarClass(achievement.id, tier)" />
</div>
<CardHeader class="pt-4">
<div class="flex items-start gap-3">
<div class="p-2 rounded-lg" :class="getIconBgClass(achievement.id)">
<component :is="getIcon(achievement.icon)" class="h-6 w-6" :class="getIconClass(achievement.id)" />
</div>
<div class="flex-1 min-w-0">
<CardTitle class="text-sm sm:text-base flex items-center gap-2">
{{ t(`achievements.names.${achievement.id}`) }}
<Badge v-if="getCurrentTier(achievement.id)" :class="getTierBadgeClass(getCurrentTier(achievement.id)!)">
{{ t(`achievements.tiers.${getCurrentTier(achievement.id)}`) }}
</Badge>
</CardTitle>
<CardDescription class="text-xs mt-1">
{{ t(`achievements.descriptions.${achievement.id}`) }}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent class="space-y-3">
<!-- 进度条 -->
<div class="space-y-1">
<div class="flex justify-between text-xs">
<span class="text-muted-foreground">{{ t('achievements.progress') }}</span>
<span class="font-medium">
{{ formatNumber(getCurrentValue(achievement.id)) }} /
{{ formatNumber(getNextTarget(achievement.id) || getCurrentValue(achievement.id)) }}
</span>
</div>
<Progress :model-value="getProgressPercentage(achievement.id)" class="h-2" />
</div>
<!-- 下一等级奖励 -->
<div v-if="getNextTierConfig(achievement.id)" class="p-2 bg-muted/50 rounded-lg">
<p class="text-xs text-muted-foreground mb-1">
{{ t('achievements.nextTier') }}: {{ t(`achievements.tiers.${getNextTierConfig(achievement.id)!.tier}`) }}
</p>
<div class="flex items-center gap-3 text-xs">
<div v-if="getNextTierConfig(achievement.id)!.reward.darkMatter" class="flex items-center gap-1">
<Sparkles class="h-3 w-3 text-purple-500" />
<span>+{{ formatNumber(getNextTierConfig(achievement.id)!.reward.darkMatter!) }}</span>
</div>
<div v-if="getNextTierConfig(achievement.id)!.reward.points" class="flex items-center gap-1">
<Star class="h-3 w-3 text-yellow-500" />
<span>+{{ formatNumber(getNextTierConfig(achievement.id)!.reward.points!) }}</span>
</div>
</div>
</div>
<!-- 已达最高等级 -->
<div
v-else-if="getCurrentTier(achievement.id) === 'diamond'"
class="p-2 bg-gradient-to-r from-purple-500/10 to-blue-500/10 rounded-lg"
>
<p class="text-xs text-center font-medium text-purple-600 dark:text-purple-400">
{{ t('achievements.maxTierReached') }}
</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useGameStore } from '@/stores/gameStore'
import { useI18n } from '@/composables/useI18n'
import { formatNumber } from '@/utils/format'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { AchievementCategory, AchievementTier, type AchievementTierConfig } from '@/types/game'
import { ACHIEVEMENTS, ACHIEVEMENT_MAP, TIER_ORDER, getNextTier } from '@/config/achievementConfig'
import { getAchievementProgress } from '@/logic/achievementLogic'
import {
Sparkles,
Star,
Gem,
Diamond,
Droplet,
Flame,
Building2,
FlaskConical,
Rocket,
Shield,
Swords,
Crown,
ShieldCheck,
Bomb,
Trash2,
Skull,
ShieldOff,
Plane,
Truck,
Package,
Flag,
Eye,
ArrowDownToLine,
Compass,
Sparkle,
Recycle,
Pickaxe,
Zap,
Fuel,
Handshake as HandshakeIcon,
Angry,
Gift,
HeartHandshake,
Target,
ScanEye,
Banknote,
BadgeDollarSign
} from 'lucide-vue-next'
const { t } = useI18n()
const gameStore = useGameStore()
const activeCategory = ref<AchievementCategory>(AchievementCategory.Resource)
const categories = [
{ value: AchievementCategory.Resource },
{ value: AchievementCategory.Building },
{ value: AchievementCategory.Combat },
{ value: AchievementCategory.Mission },
{ value: AchievementCategory.Diplomacy }
]
const tierOrder = TIER_ORDER
// 图标映射
const iconMap: Record<string, any> = {
Gem,
Diamond,
Droplet,
Sparkles,
Flame,
Building2,
FlaskConical,
Rocket,
Shield,
Swords,
Crown,
ShieldCheck,
Bomb,
Trash2,
Skull,
ShieldOff,
Plane,
Truck,
Package,
Flag,
Eye,
ArrowDownToLine,
Compass,
Sparkle,
Recycle,
Pickaxe,
Zap,
Fuel,
HandshakeIcon,
Angry,
Gift,
HeartHandshake,
Target,
ScanEye,
Banknote,
BadgeDollarSign
}
const getIcon = (iconName: string) => {
return iconMap[iconName] || Sparkles
}
// 获取成就进度
const getProgress = (achievementId: string) => {
return gameStore.player.achievements?.[achievementId]
}
const getCurrentTier = (achievementId: string) => {
return getProgress(achievementId)?.currentTier || null
}
const getCurrentValue = (achievementId: string) => {
return getProgress(achievementId)?.currentValue || 0
}
const getNextTarget = (achievementId: string) => {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return null
const currentTier = getCurrentTier(achievementId)
const nextTier = getNextTier(currentTier)
if (!nextTier) return null
const tierConfig = config.tiers.find(t => t.tier === nextTier)
return tierConfig?.target ?? null
}
const getNextTierConfig = (achievementId: string): AchievementTierConfig | null => {
const config = ACHIEVEMENT_MAP[achievementId]
if (!config) return null
const currentTier = getCurrentTier(achievementId)
const nextTier = getNextTier(currentTier)
if (!nextTier) return null
return config.tiers.find(t => t.tier === nextTier) || null
}
const getProgressPercentage = (achievementId: string) => {
const currentValue = getCurrentValue(achievementId)
const currentTier = getCurrentTier(achievementId)
return getAchievementProgress(achievementId, currentValue, currentTier)
}
// 按类别获取成就
const getAchievementsByCategory = (category: AchievementCategory) => {
return ACHIEVEMENTS.filter(a => a.category === category)
}
// 统计
const unlockedCount = computed(() => {
if (!gameStore.player.achievements) return 0
return Object.values(gameStore.player.achievements).filter(p => p.currentTier !== null).length
})
const totalCount = computed(() => ACHIEVEMENTS.length)
const getCategoryUnlockedCount = (category: AchievementCategory) => {
if (!gameStore.player.achievements) return 0
const categoryAchievements = ACHIEVEMENTS.filter(a => a.category === category)
return categoryAchievements.filter(a => {
const progress = gameStore.player.achievements?.[a.id]
return progress?.currentTier !== null
}).length
}
// 样式函数
const getTierBarClass = (achievementId: string, tier: AchievementTier) => {
const progress = getProgress(achievementId)
if (!progress) return 'bg-muted'
const tierUnlock = progress.tierUnlocks[tier]
if (tierUnlock !== null) {
// 已解锁
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-600'
case AchievementTier.Silver:
return 'bg-gray-400'
case AchievementTier.Gold:
return 'bg-yellow-500'
case AchievementTier.Platinum:
return 'bg-cyan-400'
case AchievementTier.Diamond:
return 'bg-purple-500'
}
}
return 'bg-muted'
}
const getTierBadgeClass = (tier: AchievementTier) => {
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-600 text-white'
case AchievementTier.Silver:
return 'bg-gray-400 text-white'
case AchievementTier.Gold:
return 'bg-yellow-500 text-black'
case AchievementTier.Platinum:
return 'bg-cyan-400 text-black'
case AchievementTier.Diamond:
return 'bg-gradient-to-r from-purple-500 to-blue-500 text-white'
}
}
const getIconBgClass = (achievementId: string) => {
const tier = getCurrentTier(achievementId)
if (!tier) return 'bg-muted'
switch (tier) {
case AchievementTier.Bronze:
return 'bg-amber-100 dark:bg-amber-900/30'
case AchievementTier.Silver:
return 'bg-gray-100 dark:bg-gray-800'
case AchievementTier.Gold:
return 'bg-yellow-100 dark:bg-yellow-900/30'
case AchievementTier.Platinum:
return 'bg-cyan-100 dark:bg-cyan-900/30'
case AchievementTier.Diamond:
return 'bg-purple-100 dark:bg-purple-900/30'
}
}
const getIconClass = (achievementId: string) => {
const tier = getCurrentTier(achievementId)
if (!tier) return 'text-muted-foreground'
switch (tier) {
case AchievementTier.Bronze:
return 'text-amber-600'
case AchievementTier.Silver:
return 'text-gray-500'
case AchievementTier.Gold:
return 'text-yellow-600'
case AchievementTier.Platinum:
return 'text-cyan-500'
case AchievementTier.Diamond:
return 'text-purple-500'
}
}
</script>

View File

@@ -194,6 +194,7 @@
import * as buildingValidation from '@/logic/buildingValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -227,8 +228,9 @@
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
const config = BUILDINGS.value[buildingType]
if (planet.value!.isMoon) {
// 月球只能建造月球专属建筑
return config.moonOnly === true
// 月球可以建造月球专属建筑 + 非行星专属建筑(如机器人工厂、船坞、机库等)
// OGame规则月球不能建造 planetOnly 的建筑(矿场、研究实验室、纳米工厂等)
return config.planetOnly !== true
} else {
// 行星不能建造月球专属建筑
return config.moonOnly !== true
@@ -245,6 +247,12 @@
gameStore.player.officers
)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
const cost = buildingLogic.calculateBuildingCost(buildingType, currentLevel + 1)
gameLogic.trackResourceConsumption(gameStore.player, cost)
const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return { success: true }

View File

@@ -180,6 +180,7 @@
import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -236,6 +237,11 @@
if (!currentPlanet) return { success: false }
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const totalCost = shipLogic.calculateDefenseCost(defenseType, quantity)
gameLogic.trackResourceConsumption(gameStore.player, totalCost)
const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem)
return { success: true }

View File

@@ -7,12 +7,13 @@
<!-- 标签切换 -->
<Tabs v-model="activeTab" class="w-full">
<TabsList class="grid w-full grid-cols-3">
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value">
<TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-4' : 'grid-cols-3']">
<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">
{{ gameStore.player.fleetMissions.length }}
</Badge>
<Badge v-if="tab.value === 'jumpGate' && jumpGateReady" variant="default" class="ml-1"></Badge>
</TabsTrigger>
</TabsList>
@@ -59,6 +60,80 @@
</CardContent>
</Card>
<!-- 舰队预设 -->
<Card>
<CardHeader>
<div class="flex justify-between items-center">
<div>
<CardTitle>{{ t('fleetView.fleetPresets') }}</CardTitle>
<CardDescription>{{ t('fleetView.fleetPresetsDescription') }}</CardDescription>
</div>
<Button @click="saveAsPreset" variant="outline" size="sm" :disabled="fleetPresets.length >= MAX_PRESETS">
<Save class="h-4 w-4 mr-1" />
{{ t('fleetView.savePreset') }}
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="fleetPresets.length === 0" class="text-center py-4 text-muted-foreground text-sm">
{{ t('fleetView.noPresets') }}
</div>
<div v-else class="space-y-2">
<div
v-for="preset in fleetPresets"
:key="preset.id"
class="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors"
:class="{ 'ring-2 ring-primary': editingPresetId === preset.id }"
>
<div class="flex-1 cursor-pointer" @click="loadPreset(preset)">
<div class="flex items-center gap-2">
<Star class="h-4 w-4 text-yellow-500" />
<span class="font-medium">{{ preset.name }}</span>
</div>
<div class="text-xs text-muted-foreground mt-1 flex flex-wrap gap-2">
<span v-if="preset.targetPosition">
[{{ preset.targetPosition.galaxy }}:{{ preset.targetPosition.system }}:{{ preset.targetPosition.position }}]
</span>
<span v-if="preset.missionType">
{{ getMissionName(preset.missionType) }}
</span>
<span>{{ Object.entries(preset.fleet).filter(([_, count]) => count > 0).length }} {{ t('fleetView.shipTypes') }}</span>
</div>
</div>
<div class="flex items-center gap-1">
<Button v-if="editingPresetId === preset.id" @click="updatePreset(preset.id)" variant="default" size="sm">
{{ t('common.save') }}
</Button>
<Button
v-if="editingPresetId !== preset.id"
@click="editingPresetId = preset.id"
variant="ghost"
size="sm"
:title="t('fleetView.editPreset')"
>
<Pencil class="h-4 w-4" />
</Button>
<Button @click.stop="startRenamePreset(preset)" variant="ghost" size="sm" :title="t('fleetView.renamePreset')">
<Type class="h-4 w-4" />
</Button>
<Button
@click.stop="deletePreset(preset.id)"
variant="ghost"
size="sm"
class="text-destructive hover:text-destructive"
:title="t('fleetView.deletePreset')"
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
</div>
<p v-if="editingPresetId" class="text-xs text-muted-foreground mt-2">
{{ t('fleetView.editingPresetHint') }}
</p>
</CardContent>
</Card>
<!-- 选择舰队 -->
<Card>
<CardHeader>
@@ -93,13 +168,27 @@
<CardHeader>
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
</CardHeader>
<CardContent>
<CardContent class="space-y-4">
<div class="grid grid-cols-3 gap-2 sm:gap-4">
<div v-for="coord in coordinateFields" :key="coord.key" class="space-y-2">
<Label :for="coord.key" class="text-xs sm:text-sm">{{ t(`fleetView.${coord.key}`) }}</Label>
<Input :id="coord.key" v-model.number="targetPosition[coord.key]" type="number" :min="1" :max="coord.max" placeholder="1" />
</div>
</div>
<!-- 目标类型选择行星/月球 -->
<div v-if="hasMoonAtTargetPosition" class="flex items-center gap-4 p-3 bg-muted/50 rounded-lg">
<span class="text-sm font-medium">{{ t('fleetView.targetType') }}:</span>
<div class="flex gap-2">
<Button @click="targetIsMoon = false" :variant="!targetIsMoon ? 'default' : 'outline'" size="sm">
<Globe class="h-4 w-4 mr-1" />
{{ t('fleetView.planet') }}
</Button>
<Button @click="targetIsMoon = true" :variant="targetIsMoon ? 'default' : 'outline'" size="sm">
<Moon class="h-4 w-4 mr-1" />
{{ t('fleetView.moon') }}
</Button>
</div>
</div>
</CardContent>
</Card>
@@ -278,6 +367,108 @@
</CardContent>
</Card>
</TabsContent>
<!-- 跳跃门 -->
<TabsContent v-if="showJumpGateTab" value="jumpGate" class="mt-4 space-y-4">
<!-- 跳跃门状态 -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Zap class="h-5 w-5" />
{{ t('fleetView.jumpGate') }}
</CardTitle>
<CardDescription>{{ t('fleetView.jumpGateDescription') }}</CardDescription>
</CardHeader>
<CardContent>
<!-- 冷却状态 -->
<div v-if="!jumpGateReady" class="p-4 bg-muted/50 rounded-lg">
<div class="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<Clock class="h-4 w-4" />
<span class="font-medium">{{ t('fleetView.jumpGateCooldown') }}</span>
</div>
<div class="mt-2 flex items-center gap-2">
<span class="text-sm text-muted-foreground">{{ t('fleetView.jumpGateCooldownRemaining') }}:</span>
<span class="font-bold">{{ formatTime(Math.floor(jumpGateCooldownRemaining / 1000)) }}</span>
</div>
<Progress :model-value="100 - (jumpGateCooldownRemaining / 3600000) * 100" class="mt-2" />
</div>
<!-- 就绪状态 -->
<div v-else class="p-4 bg-green-500/10 rounded-lg">
<div class="flex items-center gap-2 text-green-600 dark:text-green-400">
<Check class="h-4 w-4" />
<span class="font-medium">{{ t('fleetView.jumpGateReady') }}</span>
</div>
</div>
</CardContent>
</Card>
<!-- 选择目标月球 -->
<Card v-if="jumpGateReady">
<CardHeader>
<CardTitle>{{ t('fleetView.jumpGateSelectTarget') }}</CardTitle>
</CardHeader>
<CardContent>
<div v-if="availableJumpGateMoons.length === 0" class="text-center py-4 text-muted-foreground">
{{ t('fleetView.jumpGateNoTargetMoons') }}
</div>
<div v-else class="space-y-2">
<div
v-for="moon in availableJumpGateMoons"
:key="moon.id"
class="p-3 border rounded-lg cursor-pointer transition-colors"
:class="selectedJumpGateTarget?.id === moon.id ? 'ring-2 ring-primary bg-primary/10' : 'hover:bg-muted/50'"
@click="selectedJumpGateTarget = moon"
>
<div class="flex items-center justify-between">
<div>
<span class="font-medium">{{ moon.name }}</span>
<span class="text-sm text-muted-foreground ml-2">
[{{ moon.position.galaxy }}:{{ moon.position.system }}:{{ moon.position.position }}]
</span>
</div>
<Badge v-if="isJumpGateMoonReady(moon)" variant="default">{{ t('fleetView.jumpGateReady') }}</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- 选择传送舰队 -->
<Card v-if="jumpGateReady && selectedJumpGateTarget">
<CardHeader>
<CardTitle>{{ t('fleetView.jumpGateSelectFleet') }}</CardTitle>
</CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div v-for="(count, shipType) in planet.fleet" :key="shipType" class="space-y-2">
<Label :for="`jump-ship-${shipType}`" class="text-xs sm:text-sm">
{{ SHIPS[shipType].name }} ({{ t('fleetView.available') }}: {{ count }})
</Label>
<div class="flex gap-2">
<Input
:id="`jump-ship-${shipType}`"
v-model.number="jumpGateFleet[shipType]"
type="number"
min="0"
:max="count"
placeholder="0"
class="text-sm"
/>
<Button @click="jumpGateFleet[shipType] = count" variant="outline" size="sm">{{ t('fleetView.all') }}</Button>
</div>
</div>
</div>
<!-- 传送按钮 -->
<div class="mt-6">
<Button @click="executeJumpGateTransfer" :disabled="!canExecuteJumpGate" class="w-full">
<Zap class="h-4 w-4 mr-2" />
{{ t('fleetView.jumpGateTransfer') }}
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<!-- 提示对话框 -->
@@ -295,6 +486,40 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 预设名称对话框 -->
<AlertDialog :open="showPresetNameDialog" @update:open="showPresetNameDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{{ pendingPresetAction === 'save' ? t('fleetView.savePresetTitle') : t('fleetView.renamePresetTitle') }}
</AlertDialogTitle>
<AlertDialogDescription>
{{ pendingPresetAction === 'save' ? t('fleetView.savePresetDescription') : t('fleetView.renamePresetDescription') }}
</AlertDialogDescription>
</AlertDialogHeader>
<div class="py-4">
<Label for="preset-name">{{ t('fleetView.presetName') }}</Label>
<Input
id="preset-name"
v-model="editingPresetName"
:placeholder="t('fleetView.presetNamePlaceholder')"
class="mt-2"
@keyup.enter="handlePresetNameConfirm"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel
@click="() => { showPresetNameDialog = false; pendingPresetAction = null }"
>
{{ t('common.cancel') }}
</AlertDialogCancel>
<AlertDialogAction @click="handlePresetNameConfirm" :disabled="!editingPresetName.trim()">
{{ t('common.confirm') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
@@ -307,7 +532,7 @@
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
import type { Fleet, Resources } from '@/types/game'
import type { Fleet, Resources, FleetPreset } from '@/types/game'
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'
@@ -329,7 +554,27 @@
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift, Compass } from 'lucide-vue-next'
import {
Sword,
Package,
Rocket as RocketIcon,
Eye,
Users,
Recycle,
Skull,
Gift,
Compass,
Save,
Trash2,
Pencil,
Star,
Type,
Zap,
Clock,
Check,
Globe,
Moon
} from 'lucide-vue-next'
import { formatNumber, formatTime } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation'
import * as fleetLogic from '@/logic/fleetLogic'
@@ -337,6 +582,8 @@
import * as officerLogic from '@/logic/officerLogic'
import * as publicLogic from '@/logic/publicLogic'
import * as diplomaticLogic from '@/logic/diplomaticLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as moonLogic from '@/logic/moonLogic'
const route = useRoute()
const gameStore = useGameStore()
@@ -363,15 +610,127 @@
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
})
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
const activeTab = ref<'fleet' | 'send' | 'missions' | 'jumpGate'>('fleet')
// Tab 配置
const fleetTabs = [
{ value: 'fleet', labelKey: 'fleetOverview' },
{ value: 'send', labelKey: 'sendFleet' },
{ value: 'missions', labelKey: 'flightMissions' }
{ value: 'missions', labelKey: 'flightMissions' },
{ value: 'jumpGate', labelKey: 'jumpGate' }
] as const
// 跳跃门相关
const selectedJumpGateTarget = ref<typeof planet.value | null>(null)
const jumpGateFleet = ref<Partial<Fleet>>({
[ShipType.LightFighter]: 0,
[ShipType.HeavyFighter]: 0,
[ShipType.Cruiser]: 0,
[ShipType.Battleship]: 0,
[ShipType.SmallCargo]: 0,
[ShipType.LargeCargo]: 0,
[ShipType.ColonyShip]: 0,
[ShipType.Recycler]: 0,
[ShipType.EspionageProbe]: 0,
[ShipType.DarkMatterHarvester]: 0,
[ShipType.Deathstar]: 0
})
// 是否显示跳跃门标签页(当前在月球上且有跳跃门)
const showJumpGateTab = computed(() => {
if (!planet.value) return false
if (!planet.value.isMoon) return false
const jumpGateLevel = planet.value.buildings[BuildingType.JumpGate] || 0
return jumpGateLevel > 0
})
// 跳跃门是否就绪(冷却完成)
const jumpGateReady = computed(() => {
if (!planet.value) return false
return moonLogic.isJumpGateReady(planet.value)
})
// 跳跃门剩余冷却时间
const jumpGateCooldownRemaining = computed(() => {
if (!planet.value) return 0
return moonLogic.getJumpGateCooldownRemaining(planet.value)
})
// 可用的目标月球(有跳跃门且冷却完成的其他月球)
const availableJumpGateMoons = computed(() => {
if (!planet.value) return []
return gameStore.player.planets.filter(p => {
if (p.id === planet.value?.id) return false // 排除当前月球
if (!p.isMoon) return false
const jumpGateLevel = p.buildings[BuildingType.JumpGate] || 0
if (jumpGateLevel <= 0) return false
return moonLogic.isJumpGateReady(p)
})
})
// 检查目标月球的跳跃门是否就绪
const isJumpGateMoonReady = (moon: typeof planet.value) => {
if (!moon) return false
return moonLogic.isJumpGateReady(moon)
}
// 是否可以执行跳跃门传送
const canExecuteJumpGate = computed(() => {
if (!planet.value || !selectedJumpGateTarget.value) return false
if (!jumpGateReady.value) return false
// 检查是否选择了至少一艘舰船
const totalShips = Object.values(jumpGateFleet.value).reduce((sum, count) => sum + (count || 0), 0)
return totalShips > 0
})
// 执行跳跃门传送
const executeJumpGateTransfer = () => {
if (!planet.value || !selectedJumpGateTarget.value) return
if (!canExecuteJumpGate.value) return
const sourceMoon = planet.value
const targetMoon = selectedJumpGateTarget.value
// 转移舰队
Object.entries(jumpGateFleet.value).forEach(([shipType, count]) => {
if (count && count > 0) {
const ship = shipType as ShipType
// 从源月球扣除
if (sourceMoon.fleet[ship] >= count) {
sourceMoon.fleet[ship] -= count
// 添加到目标月球
targetMoon.fleet[ship] = (targetMoon.fleet[ship] || 0) + count
}
}
})
// 设置两个跳跃门的冷却时间
moonLogic.useJumpGate(sourceMoon)
moonLogic.useJumpGate(targetMoon)
// 重置跳跃门舰队选择
Object.keys(jumpGateFleet.value).forEach(key => {
jumpGateFleet.value[key as ShipType] = 0
})
selectedJumpGateTarget.value = null
// 显示成功对话框
alertDialogTitle.value = t('fleetView.jumpGateSuccess')
alertDialogMessage.value = t('fleetView.jumpGateSuccessMessage', {
target: `${targetMoon.name} [${targetMoon.position.galaxy}:${targetMoon.position.system}:${targetMoon.position.position}]`
})
alertDialogCallback.value = null
alertDialogOpen.value = true
}
// 可见的标签页(根据是否有跳跃门动态显示)
const visibleTabs = computed(() => {
if (showJumpGateTab.value) {
return fleetTabs
}
return fleetTabs.filter(tab => tab.value !== 'jumpGate')
})
// 选择的舰队
const selectedFleet = ref<Partial<Fleet>>({
[ShipType.LightFighter]: 0,
@@ -390,6 +749,20 @@
// 目标坐标
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
// 目标是否为月球(用于区分同坐标的行星和月球)
const targetIsMoon = ref(false)
// 检查目标位置是否有月球(玩家自己的)
const hasMoonAtTargetPosition = computed(() => {
return gameStore.player.planets.some(
p =>
p.isMoon &&
p.position.galaxy === targetPosition.value.galaxy &&
p.position.system === targetPosition.value.system &&
p.position.position === targetPosition.value.position
)
})
// 坐标字段配置
const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
{ key: 'galaxy', max: 9 },
@@ -463,6 +836,227 @@
// 是否为赠送模式
const isGiftMode = ref(false)
// 舰队预设相关状态
const MAX_PRESETS = 3
const editingPresetId = ref<string | null>(null)
const editingPresetName = ref('')
const showPresetNameDialog = ref(false)
const pendingPresetAction = ref<'save' | 'rename' | null>(null)
// 获取预设列表
const fleetPresets = computed(() => gameStore.player.fleetPresets || [])
// 生成唯一ID
const generatePresetId = (): string => {
return `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// 保存当前配置为预设
const saveAsPreset = () => {
if (fleetPresets.value.length >= MAX_PRESETS) {
alertDialogTitle.value = t('fleetView.presetLimitReached')
alertDialogMessage.value = t('fleetView.presetLimitReachedMessage', { max: MAX_PRESETS.toString() })
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
// 检查是否有选择舰船
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
if (!hasShips) {
alertDialogTitle.value = t('fleetView.presetError')
alertDialogMessage.value = t('fleetView.presetNoShips')
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
pendingPresetAction.value = 'save'
editingPresetName.value = t('fleetView.presetDefaultName', { number: (fleetPresets.value.length + 1).toString() })
showPresetNameDialog.value = true
}
// 确认保存预设
const confirmSavePreset = () => {
if (!editingPresetName.value.trim()) return
// 只保存数量大于0的舰船
const fleetToSave: Partial<Fleet> = {}
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
if (count && count > 0) {
fleetToSave[shipType as ShipType] = count
}
}
// 只保存数量大于0的资源
const cargoToSave: Partial<Resources> | undefined =
selectedMission.value === MissionType.Transport
? {
metal: cargo.value.metal || 0,
crystal: cargo.value.crystal || 0,
deuterium: cargo.value.deuterium || 0,
darkMatter: cargo.value.darkMatter || 0
}
: undefined
const newPreset: FleetPreset = {
id: generatePresetId(),
name: editingPresetName.value.trim(),
fleet: fleetToSave,
targetPosition: {
galaxy: targetPosition.value.galaxy,
system: targetPosition.value.system,
position: targetPosition.value.position
},
missionType: selectedMission.value,
cargo: cargoToSave
}
if (!gameStore.player.fleetPresets) {
gameStore.player.fleetPresets = []
}
gameStore.player.fleetPresets.push(newPreset)
showPresetNameDialog.value = false
editingPresetName.value = ''
pendingPresetAction.value = null
}
// 加载预设
const loadPreset = (preset: FleetPreset) => {
// 加载舰队配置
Object.keys(selectedFleet.value).forEach(key => {
selectedFleet.value[key as ShipType] = preset.fleet[key as ShipType] || 0
})
// 加载目标坐标
if (preset.targetPosition) {
targetPosition.value = { ...preset.targetPosition }
}
// 加载任务类型
if (preset.missionType) {
selectedMission.value = preset.missionType
}
// 加载运输资源
if (preset.cargo && preset.missionType === MissionType.Transport) {
cargo.value = {
metal: preset.cargo.metal || 0,
crystal: preset.cargo.crystal || 0,
deuterium: preset.cargo.deuterium || 0,
darkMatter: preset.cargo.darkMatter || 0,
energy: 0
}
}
}
// 更新预设(点击预设后修改内容)
const updatePreset = (presetId: string) => {
const presetIndex = gameStore.player.fleetPresets?.findIndex(p => p.id === presetId)
if (presetIndex === undefined || presetIndex === -1) return
const hasShips = Object.values(selectedFleet.value).some(count => count > 0)
if (!hasShips) {
alertDialogTitle.value = t('fleetView.presetError')
alertDialogMessage.value = t('fleetView.presetNoShips')
alertDialogCallback.value = null
alertDialogOpen.value = true
return
}
const existingPreset = gameStore.player.fleetPresets![presetIndex]
if (!existingPreset) return
// 只保存数量大于0的舰船
const fleetToSave: Partial<Fleet> = {}
for (const [shipType, count] of Object.entries(selectedFleet.value)) {
if (count && count > 0) {
fleetToSave[shipType as ShipType] = count
}
}
// 只保存数量大于0的资源
const cargoToSave: Partial<Resources> | undefined =
selectedMission.value === MissionType.Transport
? {
metal: cargo.value.metal || 0,
crystal: cargo.value.crystal || 0,
deuterium: cargo.value.deuterium || 0,
darkMatter: cargo.value.darkMatter || 0
}
: undefined
const updatedPreset: FleetPreset = {
id: existingPreset.id,
name: existingPreset.name,
fleet: fleetToSave,
targetPosition: {
galaxy: targetPosition.value.galaxy,
system: targetPosition.value.system,
position: targetPosition.value.position
},
missionType: selectedMission.value,
cargo: cargoToSave
}
gameStore.player.fleetPresets![presetIndex] = updatedPreset
editingPresetId.value = null
}
// 开始编辑预设名称
const startRenamePreset = (preset: FleetPreset) => {
// 保存要重命名的预设ID但不进入编辑内容模式
editingPresetName.value = preset.name
pendingPresetAction.value = 'rename'
// 使用临时变量存储要重命名的预设ID
renameTargetPresetId.value = preset.id
showPresetNameDialog.value = true
}
// 要重命名的预设ID与编辑预设内容分开
const renameTargetPresetId = ref<string | null>(null)
// 确认重命名预设
const confirmRenamePreset = () => {
if (!editingPresetName.value.trim() || !renameTargetPresetId.value) return
const preset = gameStore.player.fleetPresets?.find(p => p.id === renameTargetPresetId.value)
if (preset) {
preset.name = editingPresetName.value.trim()
}
showPresetNameDialog.value = false
renameTargetPresetId.value = null
editingPresetName.value = ''
pendingPresetAction.value = null
}
// 删除预设
const deletePreset = (presetId: string) => {
const preset = gameStore.player.fleetPresets?.find(p => p.id === presetId)
if (!preset) return
alertDialogTitle.value = t('fleetView.deletePresetTitle')
alertDialogMessage.value = t('fleetView.deletePresetMessage', { name: preset.name })
alertDialogCallback.value = () => {
const index = gameStore.player.fleetPresets?.findIndex(p => p.id === presetId)
if (index !== undefined && index > -1) {
gameStore.player.fleetPresets!.splice(index, 1)
}
}
alertDialogOpen.value = true
}
// 处理预设名称对话框确认
const handlePresetNameConfirm = () => {
if (pendingPresetAction.value === 'save') {
confirmSavePreset()
} else if (pendingPresetAction.value === 'rename') {
confirmRenamePreset()
}
}
// 监听目标NPC变化当目标不再是NPC时自动禁用赠送模式
watch(targetNpc, newValue => {
if (!newValue && isGiftMode.value) {
@@ -543,8 +1137,16 @@
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' }
// 检查是否派遣到自己的星球
// 回收任务部署任务除外(回收残骸可能在同位置,部署可能到自己的月球)
if (planet.value && selectedMission.value !== MissionType.Recycle && selectedMission.value !== MissionType.Deploy) {
// 回收任务部署任务和运输任务除外:
// - 回收任务:可能回收同位置的残骸
// - 部署任务:可能部署到自己的月球
// - 运输任务可能从行星向同位置的月球运输资源OGame规则允许
if (
planet.value &&
selectedMission.value !== MissionType.Recycle &&
selectedMission.value !== MissionType.Deploy &&
selectedMission.value !== MissionType.Transport
) {
const isSamePlanet =
targetPosition.value.galaxy === planet.value.position.galaxy &&
targetPosition.value.system === planet.value.position.system &&
@@ -591,7 +1193,8 @@
targetPosition: { galaxy: number; system: number; position: number },
missionType: MissionType,
fleet: Partial<Fleet>,
cargo: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
cargo: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 },
isMoonTarget: boolean = false
): boolean => {
if (!gameStore.currentPlanet) return false
const currentMissions = gameStore.player.fleetMissions.length
@@ -604,6 +1207,13 @@
gameStore.player.technologies
)
if (!validation.valid) return false
// 追踪燃料消耗(同时计入资源消耗和燃料统计)
if (validation.fuelNeeded && validation.fuelNeeded > 0) {
gameLogic.trackResourceConsumption(gameStore.player, { deuterium: validation.fuelNeeded })
gameLogic.trackFuelConsumption(gameStore.player, validation.fuelNeeded)
}
const shouldDeductCargo = missionType === MissionType.Transport
shipValidation.executeFleetDispatch(gameStore.currentPlanet, fleet, validation.fuelNeeded!, shouldDeductCargo, cargo)
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
@@ -620,6 +1230,11 @@
flightTime
)
// 如果目标是月球,设置标记
if (isMoonTarget) {
mission.targetIsMoon = true
}
// 如果是赠送模式,标记任务
if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) {
mission.isGift = true
@@ -655,7 +1270,8 @@
targetPosition.value,
selectedMission.value,
fleet,
selectedMission.value === MissionType.Transport ? cargo.value : undefined
selectedMission.value === MissionType.Transport ? cargo.value : undefined,
targetIsMoon.value
)
if (success) {

View File

@@ -229,6 +229,15 @@
</p>
</PopoverContent>
</Popover>
<!-- NPC难度等级徽章 -->
<Badge
v-if="getNpcDifficultyLevel(slot.planet) !== null"
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
class="text-xs flex-shrink-0"
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
>
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
</Badge>
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
<Badge
@@ -260,6 +269,16 @@
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
variant="outline"
class="text-xs cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 border-slate-400 dark:border-slate-600 text-slate-600 dark:text-slate-400 gap-1"
@click.stop="switchToPlanet(slot.moon.id)"
>
<Moon class="h-3 w-3" />
<span>{{ slot.moon.name }}</span>
</Badge>
</div>
</div>
<!-- 空位置 -->
@@ -345,6 +364,16 @@
<p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && canScanPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPhalanxScanDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Radar class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.phalanxScan') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="!slot.planet">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -417,6 +446,15 @@
</TooltipContent>
</Tooltip>
</TooltipProvider>
<!-- NPC难度等级徽章 -->
<Badge
v-if="getNpcDifficultyLevel(slot.planet) !== null"
:variant="getDifficultyBadgeVariant(getNpcDifficultyLevel(slot.planet))"
class="text-xs"
:class="getDifficultyLevelColor(getNpcDifficultyLevel(slot.planet))"
>
Lv.{{ getNpcDifficultyLevel(slot.planet) }}
</Badge>
<!-- 残骸场徽章 -->
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child>
@@ -450,6 +488,16 @@
</div>
</PopoverContent>
</Popover>
<!-- 月球徽章 -->
<Badge
v-if="slot.moon"
variant="outline"
class="text-xs cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 border-slate-400 dark:border-slate-600 text-slate-600 dark:text-slate-400 gap-1"
@click.stop="switchToPlanet(slot.moon.id)"
>
<Moon class="h-3 w-3" />
<span>{{ slot.moon.name }}</span>
</Badge>
</div>
<!-- PC端坐标 -->
<p class="text-xs text-muted-foreground">
@@ -539,6 +587,16 @@
<p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="slot.planet && !isMyPlanet(slot.planet) && canScanPlanet(slot.planet)">
<TooltipTrigger as-child>
<Button @click="showPhalanxScanDialog(slot.planet)" variant="outline" size="sm" class="h-8 w-8 p-0">
<Radar class="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('galaxyView.phalanxScan') }}</p>
</TooltipContent>
</Tooltip>
<Tooltip v-if="!slot.planet">
<TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -650,6 +708,97 @@
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 传感器阵列扫描对话框 -->
<Dialog :open="phalanxDialogOpen" @update:open="phalanxDialogOpen = $event">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Radar class="h-5 w-5" />
{{ t('galaxyView.phalanxScanTitle') }}
</DialogTitle>
<DialogDescription v-if="phalanxTargetPlanet">
{{
t('galaxyView.phalanxScanDescription').replace(
'{coordinates}',
`${phalanxTargetPlanet.position.galaxy}:${phalanxTargetPlanet.position.system}:${phalanxTargetPlanet.position.position}`
)
}}
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<!-- 扫描信息 -->
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ t('galaxyView.phalanxCost') }}:</span>
<div class="flex items-center gap-1">
<ResourceIcon type="deuterium" size="sm" />
<span>{{ formatNumber(PHALANX_SCAN_COST) }}</span>
</div>
</div>
<!-- 扫描按钮 -->
<Button v-if="phalanxScanResults.length === 0 && !phalanxScanning" @click="executePhalanxScan" class="w-full">
<Radar class="h-4 w-4 mr-2" />
{{ t('galaxyView.phalanxScan') }}
</Button>
<!-- 扫描中 -->
<div v-if="phalanxScanning" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<!-- 扫描结果 -->
<div v-if="!phalanxScanning && phalanxScanResults.length > 0" class="space-y-3">
<div class="text-sm font-medium">
{{ t('galaxyView.phalanxFleetDetected').replace('{count}', String(phalanxScanResults.length)) }}
</div>
<div class="space-y-2 max-h-64 overflow-y-auto">
<div v-for="fleet in phalanxScanResults" :key="fleet.id" class="p-3 border rounded-lg space-y-2 text-sm">
<div class="flex items-center justify-between">
<Badge>{{ getMissionTypeText(fleet.missionType) }}</Badge>
<Badge :variant="fleet.status === 'outbound' ? 'default' : 'secondary'">
{{ fleet.status === 'outbound' ? t('galaxyView.phalanxStatusOutbound') : t('galaxyView.phalanxStatusReturning') }}
</Badge>
</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxOrigin') }}:</span>
<span class="ml-1">
{{ formatCoords(getPlanetPositionById(fleet.originPlanetId) || { galaxy: 0, system: 0, position: 0 }) }}
</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxDestination') }}:</span>
<span class="ml-1">{{ formatCoords(fleet.targetPosition) }}</span>
</div>
<div>
<span class="text-muted-foreground">{{ t('galaxyView.phalanxArrival') }}:</span>
<span class="ml-1">{{ formatTime(Math.max(0, Math.floor((fleet.arrivalTime - Date.now()) / 1000))) }}</span>
</div>
<div v-if="fleet.returnTime">
<span class="text-muted-foreground">{{ t('galaxyView.phalanxReturn') }}:</span>
<span class="ml-1">{{ formatTime(Math.max(0, Math.floor((fleet.returnTime - Date.now()) / 1000))) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无舰队 -->
<div
v-if="!phalanxScanning && phalanxScanResults.length === 0 && phalanxDialogOpen"
class="text-center py-4 text-muted-foreground"
>
{{ t('galaxyView.phalanxNoFleets') }}
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="phalanxDialogOpen = false">{{ t('common.close') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
@@ -681,10 +830,13 @@
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb } from 'lucide-vue-next'
import { Home, Eye, Sword, Rocket, Recycle, Gift, Globe, Bomb, Moon, Radar } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic'
import { formatNumber } from '@/utils/format'
import * as moonLogic from '@/logic/moonLogic'
import { formatNumber, formatTime } from '@/utils/format'
import { BuildingType, MissionType } from '@/types/game'
import type { FleetMission } from '@/types/game'
const gameStore = useGameStore()
const universeStore = useUniverseStore()
@@ -704,6 +856,12 @@
const missileTargetPlanet = ref<Planet | null>(null)
const missileCount = ref(1)
// 传感器阵列扫描对话框状态
const phalanxDialogOpen = ref(false)
const phalanxTargetPlanet = ref<Planet | null>(null)
const phalanxScanResults = ref<FleetMission[]>([])
const phalanxScanning = ref(false)
const selectedGalaxy = ref(1)
const selectedSystem = ref(1)
const currentGalaxy = ref(1)
@@ -718,7 +876,7 @@
return npcStore.npcs.find(n => n.id === highlightNpcId.value) || null
})
const systemSlots = ref<Array<{ position: number; planet: Planet | null }>>([])
const systemSlots = ref<Array<{ position: number; planet: Planet | null; moon: Planet | null }>>([])
// 获取玩家的母星
const homePlanet = computed(() => {
@@ -770,18 +928,26 @@
}
})
const getSystemPlanets = (galaxy: number, system: number): Array<{ position: number; planet: Planet | null }> => {
const getSystemPlanets = (galaxy: number, system: number): Array<{ position: number; planet: Planet | null; moon: Planet | null }> => {
const positions = gameLogic.generateSystemPositions(galaxy, system)
return positions.map(pos => {
const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
// 先从玩家星球中查找,再从宇宙地图中查找
// 先从玩家星球中查找(非月球),再从宇宙地图中查找
const planet =
gameStore.player.planets.find(
p => p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
p => !p.isMoon && p.position.galaxy === galaxy && p.position.system === system && p.position.position === pos.position
) ||
universeStore.planets[key] ||
null
return { position: pos.position, planet }
// 查找该位置的月球(如果有星球的话)
let moon: Planet | null = null
if (planet) {
// 从玩家星球中查找月球
moon = gameStore.player.planets.find(p => p.isMoon && p.parentPlanetId === planet.id) || null
}
return { position: pos.position, planet, moon }
})
}
@@ -898,6 +1064,32 @@
return planet.name
}
// 获取NPC难度等级
const getNpcDifficultyLevel = (planet: Planet | null): number | null => {
const npc = getPlanetNPC(planet)
return npc?.difficultyLevel ?? null
}
// 获取NPC难度等级颜色
const getDifficultyLevelColor = (level: number | null): string => {
if (level === null) return 'text-muted-foreground'
if (level <= 1) return 'text-green-600 dark:text-green-400' // 新手
if (level <= 2) return 'text-lime-600 dark:text-lime-400' // 简单
if (level <= 3) return 'text-yellow-600 dark:text-yellow-400' // 普通
if (level <= 4) return 'text-orange-600 dark:text-orange-400' // 困难
if (level <= 5) return 'text-red-600 dark:text-red-400' // 专家
if (level <= 6) return 'text-purple-600 dark:text-purple-400' // 大师
return 'text-pink-600 dark:text-pink-400' // 传奇及以上
}
// 获取NPC难度等级Badge样式
const getDifficultyBadgeVariant = (level: number | null): 'default' | 'secondary' | 'destructive' | 'outline' => {
if (level === null) return 'outline'
if (level <= 2) return 'secondary'
if (level <= 4) return 'default'
return 'destructive'
}
// 切换到指定星球
const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId
@@ -1030,4 +1222,172 @@
const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}
// ========== 传感器阵列扫描功能 ==========
// 获取拥有传感器阵列的月球列表
const moonsWithPhalanx = computed(() => {
return gameStore.player.planets.filter(p => {
if (!p.isMoon) return false
const phalanxLevel = p.buildings[BuildingType.SensorPhalanx] || 0
return phalanxLevel > 0
})
})
// 检查是否可以扫描目标(需要有传感器阵列的月球在范围内)
const canScanPlanet = (targetPlanet: Planet | null): boolean => {
if (!targetPlanet) return false
if (isMyPlanet(targetPlanet)) return false
// 检查是否有月球的传感器阵列可以扫描目标
return moonsWithPhalanx.value.some(moon => {
const phalanxLevel = moon.buildings[BuildingType.SensorPhalanx] || 0
return moonLogic.isInSensorPhalanxRange(moon.position, targetPlanet.position, phalanxLevel)
})
}
// 获取可以扫描目标的月球
const getMoonForScan = (targetPlanet: Planet): Planet | null => {
return (
moonsWithPhalanx.value.find(moon => {
const phalanxLevel = moon.buildings[BuildingType.SensorPhalanx] || 0
return moonLogic.isInSensorPhalanxRange(moon.position, targetPlanet.position, phalanxLevel)
}) || null
)
}
// 计算扫描消耗的氘每次扫描消耗5000氘
const PHALANX_SCAN_COST = 5000
// 显示传感器阵列扫描对话框
const showPhalanxScanDialog = (planet: Planet) => {
phalanxTargetPlanet.value = planet
phalanxScanResults.value = []
phalanxScanning.value = false
phalanxDialogOpen.value = true
}
// 根据星球ID获取星球坐标
const getPlanetPositionById = (planetId: string): { galaxy: number; system: number; position: number } | null => {
// 先从玩家星球中查找
const playerPlanet = gameStore.player.planets.find(p => p.id === planetId)
if (playerPlanet) return playerPlanet.position
// 再从NPC星球中查找
for (const npc of npcStore.npcs) {
const npcPlanet = npc.planets.find(p => p.id === planetId)
if (npcPlanet) return npcPlanet.position
}
// 从宇宙地图中查找
for (const key in universeStore.planets) {
const planet = universeStore.planets[key]
if (planet && planet.id === planetId) return planet.position
}
return null
}
// 执行传感器阵列扫描
const executePhalanxScan = () => {
if (!phalanxTargetPlanet.value) return
const scanMoon = getMoonForScan(phalanxTargetPlanet.value)
if (!scanMoon) {
alertDialogTitle.value = t('errors.scanFailed')
alertDialogMessage.value = t('galaxyView.phalanxNoMoon')
alertDialogOpen.value = true
return
}
// 检查氘是否足够
if (scanMoon.resources.deuterium < PHALANX_SCAN_COST) {
alertDialogTitle.value = t('errors.scanFailed')
alertDialogMessage.value = t('galaxyView.phalanxInsufficientDeuterium')
alertDialogOpen.value = true
return
}
// 扣除氘
scanMoon.resources.deuterium -= PHALANX_SCAN_COST
phalanxScanning.value = true
// 模拟扫描延迟
setTimeout(() => {
// 扫描NPC的舰队任务
const targetPos = phalanxTargetPlanet.value!.position
const npc = getPlanetNPC(phalanxTargetPlanet.value)
// 收集相关的舰队任务
const detectedFleets: FleetMission[] = []
// 检查NPC的舰队任务
if (npc) {
npc.fleetMissions?.forEach(mission => {
// 获取出发地坐标
const originPos = getPlanetPositionById(mission.originPlanetId)
// 检查任务是否与目标星球相关(出发地或目的地)
const isFromTarget =
originPos &&
originPos.galaxy === targetPos.galaxy &&
originPos.system === targetPos.system &&
originPos.position === targetPos.position
const isToTarget =
mission.targetPosition.galaxy === targetPos.galaxy &&
mission.targetPosition.system === targetPos.system &&
mission.targetPosition.position === targetPos.position
if (isFromTarget || isToTarget) {
detectedFleets.push(mission)
}
})
}
// 也检查玩家自己发往该星球的任务(自己的任务自己当然知道,但扫描也能看到)
gameStore.player.fleetMissions?.forEach(mission => {
const isToTarget =
mission.targetPosition.galaxy === targetPos.galaxy &&
mission.targetPosition.system === targetPos.system &&
mission.targetPosition.position === targetPos.position
if (isToTarget) {
detectedFleets.push(mission)
}
})
phalanxScanResults.value = detectedFleets
phalanxScanning.value = false
}, 1000)
}
// 获取任务类型文本
const getMissionTypeText = (missionType: MissionType): string => {
switch (missionType) {
case MissionType.Attack:
return t('fleetView.attack')
case MissionType.Transport:
return t('fleetView.transport')
case MissionType.Deploy:
return t('fleetView.deploy')
case MissionType.Spy:
return t('fleetView.spy')
case MissionType.Colonize:
return t('fleetView.colonize')
case MissionType.Recycle:
return t('fleetView.recycle')
case MissionType.Destroy:
return t('fleetView.destroy')
case MissionType.Expedition:
return t('fleetView.expedition')
default:
return missionType
}
}
// 格式化坐标
const formatCoords = (pos: { galaxy: number; system: number; position: number }): string => {
return `[${pos.galaxy}:${pos.system}:${pos.position}]`
}
</script>

View File

@@ -176,6 +176,7 @@
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
import * as officerLogic from '@/logic/officerLogic'
import * as resourceLogic from '@/logic/resourceLogic'
import * as gameLogic from '@/logic/gameLogic'
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
@@ -250,6 +251,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false
}
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
gameStore.player.officers[officerType] = officerLogic.createActiveOfficer(officerType, duration)
return true
@@ -276,6 +279,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false
}
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
const now = Date.now()
gameStore.player.officers[officerType] = officerLogic.renewOfficerExpiration(gameStore.player.officers[officerType], duration, now)

View File

@@ -12,7 +12,6 @@
<!-- 月球信息 -->
<div v-if="!planet.isMoon && moon" class="mt-2">
<Button @click="switchToMoon" variant="outline" size="sm">
<span class="mr-2">🌙</span>
{{ t('planet.switchToMoon') }}
</Button>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="planet" class="container mx-auto p-4 sm:p-6">
<!-- 未解锁遮罩 -->
<!-- <UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" /> -->
<UnlockRequirement :required-building="BuildingType.ResearchLab" :required-level="1" />
<h1 class="text-2xl sm:text-3xl font-bold mb-4 sm:mb-6">{{ t('researchView.title') }}</h1>
@@ -107,12 +107,14 @@
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue'
import CardUnlockOverlay from '@/components/CardUnlockOverlay.vue'
import { Check, X } from 'lucide-vue-next'
import { formatNumber, getResourceCostColor } from '@/utils/format'
import * as publicLogic from '@/logic/publicLogic'
import * as researchLogic from '@/logic/researchLogic'
import * as researchValidation from '@/logic/researchValidation'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -146,6 +148,11 @@
)
if (!validation.valid) return false
const currentLevel = gameStore.player.technologies[techType] || 0
// 追踪资源消耗(在扣除前计算成本)
const cost = researchLogic.calculateTechnologyCost(techType, currentLevel + 1)
gameLogic.trackResourceConsumption(gameStore.player, cost)
const { queueItem } = researchValidation.executeTechnologyResearch(
gameStore.currentPlanet,
techType,

View File

@@ -31,13 +31,20 @@
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
<Card v-for="shipType in Object.values(ShipType)" :key="shipType" class="relative">
<CardUnlockOverlay :requirements="SHIPS[shipType].requirements" />
<CardHeader class="pb-3">
<CardTitle
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 mb-2"
@click="detailDialog.openShip(shipType)"
>
{{ SHIPS[shipType].name }}
</CardTitle>
<CardHeader>
<div class="mb-2">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
<CardTitle
class="text-sm sm:text-base lg:text-lg cursor-pointer hover:text-primary transition-colors underline decoration-dotted underline-offset-4 order-2 sm:order-1"
@click="detailDialog.openShip(shipType)"
>
{{ SHIPS[shipType].name }}
</CardTitle>
<Badge variant="secondary" class="text-xs whitespace-nowrap self-start order-1 sm:order-2">
{{ formatNumber(planet.fleet[shipType] || 0) }}
</Badge>
</div>
</div>
<CardDescription class="text-xs sm:text-sm">{{ SHIPS[shipType].description }}</CardDescription>
</CardHeader>
<CardContent>
@@ -146,6 +153,7 @@
import { computed, ref } from 'vue'
import { ShipType, BuildingType } from '@/types/game'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -165,6 +173,8 @@
import * as shipValidation from '@/logic/shipValidation'
import * as publicLogic from '@/logic/publicLogic'
import * as fleetStorageLogic from '@/logic/fleetStorageLogic'
import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore()
const detailDialog = useDetailDialogStore()
@@ -220,6 +230,11 @@
if (!gameStore.currentPlanet) return { success: false }
const validation = shipValidation.validateShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.technologies)
if (!validation.valid) return { success: false, reason: validation.reason }
// 追踪资源消耗(在扣除前计算成本)
const totalCost = shipLogic.calculateShipCost(shipType, quantity)
gameLogic.trackResourceConsumption(gameStore.player, totalCost)
const queueItem = shipValidation.executeShipBuild(gameStore.currentPlanet, shipType, quantity, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem)
return { success: true }