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 }} name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ matrix.executable }} path: ${{ matrix.executable }}
# 2. 构建 Electron 客户端 # 2. 构建 Android APK (ARM64, ARMv7, x86_64)
build-android:
name: Build Android APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 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: build-electron:
name: Build Electron (${{ matrix.os }}) name: Build Electron (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -91,9 +131,9 @@ jobs:
pkg/*.dmg pkg/*.dmg
pkg/*.AppImage pkg/*.AppImage
# 3. 发布 Release # 4. 发布 Release
release: release:
needs: [ build-server, build-electron ] needs: [ build-server, build-android, build-electron ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -124,6 +164,9 @@ jobs:
# 移动 Electron 安装包 (排除 unpacked 目录) # 移动 Electron 安装包 (排除 unpacked 目录)
find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \; find ./raw-assets/electron-* -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.zip" \) -exec cp {} ./final-release/ \;
# 移动 Android APK
find ./raw-assets/android-apk -type f -name "*.apk" -exec cp {} ./final-release/ \; || true
# 检查结果 # 检查结果
echo "Final assets to upload:" echo "Final assets to upload:"
ls -R ./final-release ls -R ./final-release

9
.gitignore vendored
View File

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

View File

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

View File

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

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

1406
pnpm-lock.yaml generated
View File

@@ -11,6 +11,15 @@ importers:
.: .:
dependencies: 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': '@tailwindcss/vite':
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1)) version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.2)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.44.1))
@@ -96,6 +105,12 @@ importers:
electron-vite: electron-vite:
specifier: ^5.0.0 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)) 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: tailwind-merge:
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.4.0 version: 3.4.0
@@ -630,6 +645,307 @@ packages:
'@canvas/image-data@1.1.0': '@canvas/image-data@1.1.0':
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==} resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
'@capacitor/android@8.0.0':
resolution: {integrity: sha512-FrBSvVAC5JuLaYHNyDnwQny0/SYnP+xDQbc/KA4wInmRkMXLDv22fkx9aBJIDrxjuUVd+jsRih4SAt8FgMEzCw==}
peerDependencies:
'@capacitor/core': ^8.0.0
'@capacitor/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': '@develar/schema-utils@2.6.5':
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'} engines: {node: '>= 8.9.0'}
@@ -984,6 +1300,38 @@ packages:
'@internationalized/number@3.6.5': '@internationalized/number@3.6.5':
resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
'@ionic/cli-framework-output@2.2.8':
resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==}
engines: {node: '>=16.0.0'}
'@ionic/utils-array@2.1.6':
resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-fs@3.1.7':
resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==}
engines: {node: '>=16.0.0'}
'@ionic/utils-object@2.1.6':
resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==}
engines: {node: '>=16.0.0'}
'@ionic/utils-process@2.1.12':
resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==}
engines: {node: '>=16.0.0'}
'@ionic/utils-stream@3.1.7':
resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==}
engines: {node: '>=16.0.0'}
'@ionic/utils-subprocess@3.0.1':
resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==}
engines: {node: '>=16.0.0'}
'@ionic/utils-terminal@2.3.5':
resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==}
engines: {node: '>=16.0.0'}
'@isaacs/balanced-match@4.0.1': '@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -1339,6 +1687,9 @@ packages:
'@types/file-saver@2.0.7': '@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/fs-extra@8.1.5':
resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==}
'@types/fs-extra@9.0.13': '@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
@@ -1366,6 +1717,9 @@ packages:
'@types/responselike@1.0.3': '@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
'@types/slice-ansi@4.0.0':
resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==}
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1590,6 +1944,13 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'} 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: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1619,6 +1980,10 @@ packages:
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true hasBin: true
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
birpc@2.9.0: birpc@2.9.0:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
@@ -1629,6 +1994,10 @@ packages:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
bplist-parser@0.3.2:
resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
engines: {node: '>= 5.10.0'}
brace-expansion@1.1.12: brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -1757,6 +2126,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
commander@2.20.3: commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -1821,6 +2194,32 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'} 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: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1872,6 +2271,10 @@ packages:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
define-properties@1.2.1: define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1963,6 +2366,10 @@ packages:
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
hasBin: true hasBin: true
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2106,6 +2513,9 @@ packages:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.23.12: framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies: peerDependencies:
@@ -2206,6 +2616,10 @@ packages:
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
hasBin: true hasBin: true
glob@13.0.0:
resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
engines: {node: 20 || >=22}
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Glob versions prior to v9 are no longer supported
@@ -2337,6 +2751,10 @@ packages:
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@4.1.3:
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
internal-slot@1.1.0: internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2384,6 +2802,11 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
is-finalizationregistry@1.1.1: is-finalizationregistry@1.1.1:
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2474,6 +2897,10 @@ packages:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'} engines: {node: '>=18'}
is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@2.0.5: isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2549,6 +2976,14 @@ packages:
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
lazy-val@1.0.5: lazy-val@1.0.5:
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
@@ -2815,6 +3250,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
native-run@2.0.1:
resolution: {integrity: sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==}
engines: {node: '>=16.0.0'}
hasBin: true
negotiator@0.6.4: negotiator@0.6.4:
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -2867,6 +3307,10 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'} engines: {node: '>=6'}
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
ora@5.4.1: ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2968,6 +3412,166 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} 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: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -3005,6 +3609,10 @@ packages:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'} engines: {node: '>=10'}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
pump@3.0.3: pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3109,6 +3717,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true hasBin: true
rimraf@6.1.2:
resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==}
engines: {node: 20 || >=22}
hasBin: true
roarr@2.15.4: roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -3184,6 +3797,9 @@ packages:
sanitize-filename@1.6.3: sanitize-filename@1.6.3:
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
sax@1.1.4:
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
sax@1.4.3: sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
@@ -3278,10 +3894,17 @@ packages:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slice-ansi@3.0.0: slice-ansi@3.0.0:
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
smart-buffer@4.2.0: smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -3321,6 +3944,10 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.1.3: sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
@@ -3433,6 +4060,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
tiny-async-pool@1.3.0: tiny-async-pool@1.3.0:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
@@ -3457,6 +4087,10 @@ packages:
tr46@1.0.1: tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
truncate-utf8-bytes@1.0.2: truncate-utf8-bytes@1.0.2:
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
@@ -3547,6 +4181,10 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
upath@1.2.0: upath@1.2.0:
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -3730,6 +4368,14 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlbuilder@15.1.1: xmlbuilder@15.1.1:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -4427,6 +5073,334 @@ snapshots:
'@canvas/image-data@1.1.0': '@canvas/image-data@1.1.0':
optional: true optional: true
'@capacitor/android@8.0.0(@capacitor/core@8.0.0)':
dependencies:
'@capacitor/core': 8.0.0
'@capacitor/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': '@develar/schema-utils@2.6.5':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
@@ -4741,6 +5715,82 @@ snapshots:
dependencies: dependencies:
'@swc/helpers': 0.5.17 '@swc/helpers': 0.5.17
'@ionic/cli-framework-output@2.2.8':
dependencies:
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-array@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-fs@3.1.7':
dependencies:
'@types/fs-extra': 8.1.5
debug: 4.4.3
fs-extra: 9.1.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-object@2.1.6':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-process@2.1.12':
dependencies:
'@ionic/utils-object': 2.1.6
'@ionic/utils-terminal': 2.3.5
debug: 4.4.3
signal-exit: 3.0.7
tree-kill: 1.2.2
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-stream@3.1.7':
dependencies:
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-subprocess@3.0.1':
dependencies:
'@ionic/utils-array': 2.1.6
'@ionic/utils-fs': 3.1.7
'@ionic/utils-process': 2.1.12
'@ionic/utils-stream': 3.1.7
'@ionic/utils-terminal': 2.3.5
cross-spawn: 7.0.6
debug: 4.4.3
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
'@ionic/utils-terminal@2.3.5':
dependencies:
'@types/slice-ansi': 4.0.0
debug: 4.4.3
signal-exit: 3.0.7
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
tslib: 2.8.1
untildify: 4.0.0
wrap-ansi: 7.0.0
transitivePeerDependencies:
- supports-color
'@isaacs/balanced-match@4.0.1': {} '@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0': '@isaacs/brace-expansion@5.0.0':
@@ -5041,6 +6091,10 @@ snapshots:
'@types/file-saver@2.0.7': {} '@types/file-saver@2.0.7': {}
'@types/fs-extra@8.1.5':
dependencies:
'@types/node': 24.10.2
'@types/fs-extra@9.0.13': '@types/fs-extra@9.0.13':
dependencies: dependencies:
'@types/node': 24.10.2 '@types/node': 24.10.2
@@ -5073,6 +6127,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.10.2 '@types/node': 24.10.2
'@types/slice-ansi@4.0.0': {}
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@types/verror@1.10.11': '@types/verror@1.10.11':
@@ -5354,8 +6410,7 @@ snapshots:
assert-plus@1.0.0: assert-plus@1.0.0:
optional: true optional: true
astral-regex@2.0.0: astral-regex@2.0.0: {}
optional: true
async-exit-hook@2.0.1: {} async-exit-hook@2.0.1: {}
@@ -5367,6 +6422,15 @@ snapshots:
at-least-node@1.0.0: {} 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: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
@@ -5401,6 +6465,8 @@ snapshots:
baseline-browser-mapping@2.9.7: {} baseline-browser-mapping@2.9.7: {}
big-integer@1.6.52: {}
birpc@2.9.0: {} birpc@2.9.0: {}
bl@4.1.0: bl@4.1.0:
@@ -5412,6 +6478,10 @@ snapshots:
boolean@3.2.0: boolean@3.2.0:
optional: true optional: true
bplist-parser@0.3.2:
dependencies:
big-integer: 1.6.52
brace-expansion@1.1.12: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -5591,6 +6661,8 @@ snapshots:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
commander@12.1.0: {}
commander@2.20.3: {} commander@2.20.3: {}
commander@5.1.0: {} commander@5.1.0: {}
@@ -5647,6 +6719,26 @@ snapshots:
crypto-random-string@2.0.0: {} 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: {} csstype@3.2.3: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@@ -5702,6 +6794,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
define-lazy-prop@2.0.0: {}
define-properties@1.2.1: define-properties@1.2.1:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -5845,6 +6939,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
elementtree@0.1.7:
dependencies:
sax: 1.1.4
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
@@ -6059,6 +7157,8 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
fraction.js@5.3.4: {}
framer-motion@12.23.12: framer-motion@12.23.12:
dependencies: dependencies:
motion-dom: 12.23.12 motion-dom: 12.23.12
@@ -6174,6 +7274,12 @@ snapshots:
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 2.0.1 path-scurry: 2.0.1
glob@13.0.0:
dependencies:
minimatch: 10.1.1
minipass: 7.1.2
path-scurry: 2.0.1
glob@7.2.3: glob@7.2.3:
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
@@ -6332,6 +7438,8 @@ snapshots:
inherits@2.0.4: {} inherits@2.0.4: {}
ini@4.1.3: {}
internal-slot@1.1.0: internal-slot@1.1.0:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -6387,6 +7495,8 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
is-docker@2.2.1: {}
is-finalizationregistry@1.1.1: is-finalizationregistry@1.1.1:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -6465,6 +7575,10 @@ snapshots:
is-what@5.5.0: {} is-what@5.5.0: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
isarray@2.0.5: {} isarray@2.0.5: {}
isbinaryfile@4.0.10: {} isbinaryfile@4.0.10: {}
@@ -6528,6 +7642,10 @@ snapshots:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
kleur@3.0.3: {}
kleur@4.1.5: {}
lazy-val@1.0.5: {} lazy-val@1.0.5: {}
leven@3.1.0: {} leven@3.1.0: {}
@@ -6758,6 +7876,22 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
native-run@2.0.1:
dependencies:
'@ionic/utils-fs': 3.1.7
'@ionic/utils-terminal': 2.3.5
bplist-parser: 0.3.2
debug: 4.4.3
elementtree: 0.1.7
ini: 4.1.3
plist: 3.1.0
split2: 4.2.0
through2: 4.0.2
tslib: 2.8.1
yauzl: 2.10.0
transitivePeerDependencies:
- supports-color
negotiator@0.6.4: {} negotiator@0.6.4: {}
node-abi@3.85.0: node-abi@3.85.0:
@@ -6806,6 +7940,12 @@ snapshots:
dependencies: dependencies:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
ora@5.4.1: ora@5.4.1:
dependencies: dependencies:
bl: 4.1.0 bl: 4.1.0
@@ -6889,6 +8029,231 @@ snapshots:
possible-typed-array-names@1.1.0: {} 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: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@@ -6915,6 +8280,11 @@ snapshots:
err-code: 2.0.3 err-code: 2.0.3
retry: 0.12.0 retry: 0.12.0
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
pump@3.0.3: pump@3.0.3:
dependencies: dependencies:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
@@ -7040,6 +8410,11 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
rimraf@6.1.2:
dependencies:
glob: 13.0.0
package-json-from-dist: 1.0.1
roarr@2.15.4: roarr@2.15.4:
dependencies: dependencies:
boolean: 3.2.0 boolean: 3.2.0
@@ -7117,6 +8492,8 @@ snapshots:
dependencies: dependencies:
truncate-utf8-bytes: 1.0.2 truncate-utf8-bytes: 1.0.2
sax@1.1.4: {}
sax@1.4.3: {} sax@1.4.3: {}
semver-compare@1.0.0: semver-compare@1.0.0:
@@ -7267,6 +8644,8 @@ snapshots:
dependencies: dependencies:
semver: 7.7.3 semver: 7.7.3
sisteransi@1.0.5: {}
slice-ansi@3.0.0: slice-ansi@3.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@@ -7274,6 +8653,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0 is-fullwidth-code-point: 3.0.0
optional: true optional: true
slice-ansi@4.0.0:
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
smart-buffer@4.2.0: {} smart-buffer@4.2.0: {}
smob@1.5.0: {} smob@1.5.0: {}
@@ -7308,6 +8693,8 @@ snapshots:
speakingurl@14.0.1: {} speakingurl@14.0.1: {}
split2@4.2.0: {}
sprintf-js@1.1.3: sprintf-js@1.1.3:
optional: true optional: true
@@ -7452,6 +8839,10 @@ snapshots:
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
tiny-async-pool@1.3.0: tiny-async-pool@1.3.0:
dependencies: dependencies:
semver: 5.7.2 semver: 5.7.2
@@ -7476,6 +8867,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
tree-kill@1.2.2: {}
truncate-utf8-bytes@1.0.2: truncate-utf8-bytes@1.0.2:
dependencies: dependencies:
utf8-byte-length: 1.0.5 utf8-byte-length: 1.0.5
@@ -7577,6 +8970,8 @@ snapshots:
universalify@2.0.1: {} universalify@2.0.1: {}
untildify@4.0.0: {}
upath@1.2.0: {} upath@1.2.0: {}
update-browserslist-db@1.2.2(browserslist@4.28.1): update-browserslist-db@1.2.2(browserslist@4.28.1):
@@ -7832,6 +9227,13 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
xml2js@0.6.2:
dependencies:
sax: 1.4.3
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
xmlbuilder@15.1.1: {} xmlbuilder@15.1.1: {}
y18n@5.0.8: {} y18n@5.0.8: {}

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

View File

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

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> <CardHeader>
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<CardTitle class="flex items-center gap-2"> <CardTitle class="flex items-center gap-2 flex-wrap">
{{ npc.name }} {{ npc.name }}
<span v-if="npc.note" class="text-muted-foreground font-normal">({{ npc.note }})</span> <span v-if="npc.note" class="text-muted-foreground font-normal">({{ npc.note }})</span>
<Badge :variant="statusBadgeVariant"> <Badge :variant="statusBadgeVariant">
{{ statusText }} {{ statusText }}
</Badge> </Badge>
<!-- NPC难度等级徽章 -->
<Badge
v-if="npc.difficultyLevel"
:variant="difficultyBadgeVariant"
:class="difficultyLevelColor"
>
Lv.{{ npc.difficultyLevel }}
</Badge>
</CardTitle> </CardTitle>
<CardDescription class="mt-1"> <CardDescription class="mt-1">
{{ npc.planets.length }} {{ t('diplomacy.planets') }} {{ npc.planets.length }} {{ t('diplomacy.planets') }}
@@ -208,6 +216,28 @@
return 'text-muted-foreground' 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(() => { const recentEvent = computed(() => {
if (!props.relation?.history || props.relation.history.length === 0) return null if (!props.relation?.history || props.relation.history.length === 0) return null

View File

@@ -18,6 +18,15 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-medium truncate">{{ npc.name }}</span> <span class="font-medium truncate">{{ npc.name }}</span>
<span v-if="npc.note" class="text-muted-foreground text-sm truncate">({{ npc.note }})</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>
<div class="text-xs text-muted-foreground"> <div class="text-xs text-muted-foreground">
{{ npc.planets.length }} {{ t('diplomacy.planets') }} {{ npc.planets.length }} {{ t('diplomacy.planets') }}
@@ -68,9 +77,18 @@
'bg-gray-400': status === RelationStatus.Neutral '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 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> </div>
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" /> <ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" />
</div> </div>
@@ -214,6 +232,28 @@
return 'text-muted-foreground' 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(() => { const recentEvent = computed(() => {
if (!props.relation?.history || props.relation.history.length === 0) return null if (!props.relation?.history || props.relation.history.length === 0) return null

View File

@@ -173,7 +173,8 @@
const getQueueProgress = (item: BuildQueueItem): number => { const getQueueProgress = (item: BuildQueueItem): number => {
const elapsed = currentTime.value - item.startTime const elapsed = currentTime.value - item.startTime
const total = item.endTime - 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', icon: 'swords',
delay: 500 delay: 500
}, },
{
id: 'achievements_intro',
route: '/achievements',
titleKey: 'hints.achievements.title',
messageKey: 'hints.achievements.message',
icon: 'trophy',
delay: 500
},
{ {
id: 'settings_intro', id: 'settings_intro',
route: '/settings', 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秒 baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 1.5, costMultiplier: 1.5,
spaceUsage: 2, spaceUsage: 2,
planetOnly: true, // OGame规则月球不能建造太阳能电站
levelRequirements: { levelRequirements: {
15: { [BuildingType.RoboticsFactory]: 3 }, 15: { [BuildingType.RoboticsFactory]: 3 },
25: { [BuildingType.RoboticsFactory]: 6, [BuildingType.ResearchLab]: 5 }, 25: { [BuildingType.RoboticsFactory]: 6, [BuildingType.ResearchLab]: 5 },
@@ -73,6 +74,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 30, baseTime: 30,
costMultiplier: 1.8, costMultiplier: 1.8,
spaceUsage: 4, spaceUsage: 4,
planetOnly: true, // OGame规则月球不能建造核聚变反应堆
requirements: { requirements: {
[TechnologyType.EnergyTechnology]: 3, [TechnologyType.EnergyTechnology]: 3,
[BuildingType.DeuteriumSynthesizer]: 5 [BuildingType.DeuteriumSynthesizer]: 5
@@ -109,6 +111,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 8, spaceUsage: 8,
maxLevel: 10, // 最多10级最多11个建造队列 maxLevel: 10, // 最多10级最多11个建造队列
planetOnly: true, // OGame规则月球不能建造纳米工厂
requirements: { [BuildingType.RoboticsFactory]: 10 }, requirements: { [BuildingType.RoboticsFactory]: 10 },
levelRequirements: { levelRequirements: {
3: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 8, [TechnologyType.ComputerTechnology]: 8 }, 3: { [BuildingType.ResearchLab]: 10, [BuildingType.Shipyard]: 8, [TechnologyType.ComputerTechnology]: 8 },
@@ -139,6 +142,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 1.8, costMultiplier: 1.8,
spaceUsage: 3, spaceUsage: 3,
fleetStorageBonus: 1500, // 每级增加1500舰队仓储比船坞更高 fleetStorageBonus: 1500, // 每级增加1500舰队仓储比船坞更高
planetOnly: true, // OGame规则月球不能建造机库
requirements: { [BuildingType.RoboticsFactory]: 1 }, // 只需要1级机器人工厂 requirements: { [BuildingType.RoboticsFactory]: 1 }, // 只需要1级机器人工厂
levelRequirements: { levelRequirements: {
10: { [BuildingType.RoboticsFactory]: 3 }, 10: { [BuildingType.RoboticsFactory]: 3 },
@@ -153,6 +157,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 30, // 减少建造时间60→30秒 baseTime: 30, // 减少建造时间60→30秒
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 3, spaceUsage: 3,
planetOnly: true, // OGame规则月球不能建造研究实验室
requirements: { requirements: {
[BuildingType.MetalMine]: 3, [BuildingType.MetalMine]: 3,
[BuildingType.CrystalMine]: 3, [BuildingType.CrystalMine]: 3,
@@ -176,11 +181,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒 baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 1, spaceUsage: 1,
requirements: { [BuildingType.MetalMine]: 2 }, planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
levelRequirements: { requirements: { [BuildingType.MetalMine]: 1 }
8: { [BuildingType.MetalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.MetalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
}, },
[BuildingType.CrystalStorage]: { [BuildingType.CrystalStorage]: {
id: BuildingType.CrystalStorage, id: BuildingType.CrystalStorage,
@@ -190,11 +192,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒 baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 1, spaceUsage: 1,
requirements: { [BuildingType.CrystalMine]: 2 }, planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
levelRequirements: { requirements: { [BuildingType.CrystalMine]: 1 }
8: { [BuildingType.CrystalMine]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.CrystalMine]: 25, [BuildingType.RoboticsFactory]: 6 }
}
}, },
[BuildingType.DeuteriumTank]: { [BuildingType.DeuteriumTank]: {
id: BuildingType.DeuteriumTank, id: BuildingType.DeuteriumTank,
@@ -204,11 +203,8 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
baseTime: 15, // 减少建造时间30→15秒 baseTime: 15, // 减少建造时间30→15秒
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 1, spaceUsage: 1,
requirements: { [BuildingType.DeuteriumSynthesizer]: 2 }, planetOnly: true, // OGame规则月球不能建造仓储月球没有矿场
levelRequirements: { requirements: { [BuildingType.DeuteriumSynthesizer]: 1 }
8: { [BuildingType.DeuteriumSynthesizer]: 15, [BuildingType.RoboticsFactory]: 3 },
12: { [BuildingType.DeuteriumSynthesizer]: 25, [BuildingType.RoboticsFactory]: 6 }
}
}, },
[BuildingType.DarkMatterCollector]: { [BuildingType.DarkMatterCollector]: {
id: BuildingType.DarkMatterCollector, id: BuildingType.DarkMatterCollector,
@@ -255,6 +251,7 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
costMultiplier: 2, costMultiplier: 2,
spaceUsage: 5, spaceUsage: 5,
maxLevel: 10, maxLevel: 10,
planetOnly: true, // OGame规则月球不能建造导弹发射井
requirements: { requirements: {
[BuildingType.Shipyard]: 1 [BuildingType.Shipyard]: 1
}, },
@@ -1097,12 +1094,18 @@ export const OFFICERS: Record<OfficerType, OfficerConfig> = {
// 月球配置 // 月球配置
export const MOON_CONFIG = { export const MOON_CONFIG = {
minDebrisField: 100000, // 最小残骸场 (金属+晶体) minDebrisField: 100000, // 最小残骸场 (金属+晶体) 100k = 1%概率
baseChance: 1, // 基础1%概率 baseChance: 0, // 基础0%概率每100k残骸增加1%
maxChance: 20, // 最大20%概率 maxChance: 20, // 最大20%概率需要2M残骸
chancePerDebris: 100000, // 每10万资源增加1%概率 chancePerDebris: 100000, // 每10万资源增加1%概率
baseSize: 100, // 月球基础空间 baseFields: 1, // 月球初始空间OGame规则初始只有1格
lunarBaseSpaceBonus: 30 // 每级月球基地增加的空间 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', officers: 'Offiziere',
simulator: 'Simulator', simulator: 'Simulator',
galaxy: 'Galaxie', galaxy: 'Galaxie',
diplomacy: 'Diplomacy', diplomacy: 'Diplomatie',
achievements: 'Erfolge',
messages: 'Nachrichten', messages: 'Nachrichten',
settings: 'Einstellungen', settings: 'Einstellungen',
gm: 'GM' gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: 'Stunde', hour: 'Stunde',
noEnergy: 'Keine Energie' 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: 'Planet', planet: 'Planet',
moon: 'Mond', moon: 'Mond',
@@ -421,6 +430,7 @@ export default {
shipyardView: { shipyardView: {
title: 'Raumschiffwerft', title: 'Raumschiffwerft',
fleetStorage: 'Flottenspeicher', fleetStorage: 'Flottenspeicher',
owned: 'Besitz',
attack: 'Angriff', attack: 'Angriff',
missileAttack: 'Raketenangriff', missileAttack: 'Raketenangriff',
shield: 'Schild', shield: 'Schild',
@@ -486,8 +496,10 @@ export default {
available: 'Verfügbar', available: 'Verfügbar',
all: 'Alle', all: 'Alle',
targetCoordinates: 'Zielkoordinaten', targetCoordinates: 'Zielkoordinaten',
targetType: 'Zieltyp',
planet: 'Planet',
moon: 'Mond',
galaxy: 'Galaxie', galaxy: 'Galaxie',
diplomacy: 'Diplomacy',
system: 'System', system: 'System',
position: 'Position', position: 'Position',
missionType: 'Missionstyp', missionType: 'Missionstyp',
@@ -531,7 +543,30 @@ export default {
noDeathstar: 'Todesstern für Zerstörungsmission erforderlich', noDeathstar: 'Todesstern für Zerstörungsmission erforderlich',
giftMode: 'Geschenkmodus', giftMode: 'Geschenkmodus',
giftModeDescription: 'Ressourcen als Geschenk senden an', 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: { officersView: {
title: 'Offiziere', title: 'Offiziere',
@@ -571,7 +606,6 @@ export default {
title: 'Galaxie', title: 'Galaxie',
selectCoordinates: 'Koordinaten auswählen', selectCoordinates: 'Koordinaten auswählen',
galaxy: 'Galaxie', galaxy: 'Galaxie',
diplomacy: 'Diplomacy',
selectGalaxy: 'Galaxie auswählen', selectGalaxy: 'Galaxie auswählen',
system: 'System', system: 'System',
selectSystem: 'System auswählen', selectSystem: 'System auswählen',
@@ -733,14 +767,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: 'Transportmission erfolgreich abgeschlossen', transportSuccess: 'Transportmission erfolgreich abgeschlossen',
transportFailed: 'Transportmission fehlgeschlagen', transportFailed: 'Transportmission fehlgeschlagen',
transportFailedTargetNotFound: 'Transport fehlgeschlagen: Zielplanet existiert nicht',
transportFailedGiftRejected: 'Transport fehlgeschlagen: Geschenk wurde abgelehnt',
colonizeSuccess: 'Kolonisierungsmission erfolgreich, neuer Planet gegründet', colonizeSuccess: 'Kolonisierungsmission erfolgreich, neuer Planet gegründet',
colonizeFailed: 'Kolonisierungsmission fehlgeschlagen', 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', deploySuccess: 'Einsatzmission erfolgreich abgeschlossen',
deployFailed: 'Einsatzmission fehlgeschlagen', deployFailed: 'Einsatzmission fehlgeschlagen',
deployFailedTargetNotFound: 'Einsatz fehlgeschlagen: Zielplanet existiert nicht',
deployFailedNotOwnPlanet: 'Einsatz fehlgeschlagen: Zielplanet gehört nicht dir',
recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen', recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen',
recycleFailed: 'Recyclingmission fehlgeschlagen, keine Trümmer am Zielort', 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', destroySuccess: 'Planetenzerstörungsmission erfolgreich ausgeführt',
destroyFailed: 'Planetenzerstörungsmission fehlgeschlagen', 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', missileAttackSuccess: 'Raketenangriff erfolgreich',
missileAttackFailed: 'Raketenangriff fehlgeschlagen, Zielplanet existiert nicht', missileAttackFailed: 'Raketenangriff fehlgeschlagen, Zielplanet existiert nicht',
missileAttackIntercepted: 'Alle Raketen abgefangen', missileAttackIntercepted: 'Alle Raketen abgefangen',
@@ -1259,6 +1308,10 @@ export default {
message: message:
'Simulieren Sie Kampfergebnisse vor dem Angriff. Geben Sie Flotten und Technologiestufen ein, um Sieg, Verluste und Beute vorherzusagen.' '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: { settings: {
title: 'Einstellungen', title: 'Einstellungen',
message: 'Verwalten Sie hier Spieldaten, Benachrichtigungen und Import/Export. Sichern Sie regelmäßig Ihren Fortschritt!' message: 'Verwalten Sie hier Spieldaten, Benachrichtigungen und Import/Export. Sichern Sie regelmäßig Ihren Fortschritt!'
@@ -1268,5 +1321,102 @@ export default {
message: 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.' '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', simulator: 'Simulator',
galaxy: 'Galaxy', galaxy: 'Galaxy',
diplomacy: 'Diplomacy', diplomacy: 'Diplomacy',
achievements: 'Achievements',
messages: 'Messages', messages: 'Messages',
settings: 'Settings', settings: 'Settings',
gm: 'GM' gm: 'GM'
@@ -108,6 +109,14 @@ export default {
hour: 'hour', hour: 'hour',
noEnergy: 'No Energy' 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: 'Planet', planet: 'Planet',
moon: 'Moon', moon: 'Moon',
@@ -419,6 +428,7 @@ export default {
shipyardView: { shipyardView: {
title: 'Shipyard', title: 'Shipyard',
fleetStorage: 'Fleet Storage', fleetStorage: 'Fleet Storage',
owned: 'Owned',
attack: 'Attack', attack: 'Attack',
missileAttack: 'Missile Attack', missileAttack: 'Missile Attack',
shield: 'Shield', shield: 'Shield',
@@ -482,6 +492,9 @@ export default {
available: 'Available', available: 'Available',
all: 'All', all: 'All',
targetCoordinates: 'Target Coordinates', targetCoordinates: 'Target Coordinates',
targetType: 'Target Type',
planet: 'Planet',
moon: 'Moon',
galaxy: 'Galaxy', galaxy: 'Galaxy',
system: 'System', system: 'System',
position: 'Position', position: 'Position',
@@ -528,7 +541,47 @@ export default {
noDeathstar: 'Deathstar required for destruction mission', noDeathstar: 'Deathstar required for destruction mission',
giftMode: 'Gift Mode', giftMode: 'Gift Mode',
giftModeDescription: 'Send resources as a gift to', 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: { officersView: {
title: 'Officers', title: 'Officers',
@@ -616,7 +669,26 @@ export default {
giftPlanetTitle: 'Send Gift', giftPlanetTitle: 'Send Gift',
giftPlanetMessage: 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.', '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: { messagesView: {
title: 'Messages', title: 'Messages',
@@ -726,14 +798,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: 'Transport mission completed successfully', transportSuccess: 'Transport mission completed successfully',
transportFailed: 'Transport mission failed', 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', colonizeSuccess: 'Colonization mission successful, new planet established',
colonizeFailed: 'Colonization mission failed', 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', deploySuccess: 'Deployment mission completed successfully',
deployFailed: 'Deployment mission failed', 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', recycleSuccess: 'Recycling mission completed successfully',
recycleFailed: 'Recycling mission failed, no debris at target location', 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', destroySuccess: 'Planet destruction mission executed successfully',
destroyFailed: 'Planet destruction mission failed', 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', missileAttackSuccess: 'Missile attack successful',
missileAttackFailed: 'Missile attack failed, target planet does not exist', missileAttackFailed: 'Missile attack failed, target planet does not exist',
missileAttackIntercepted: 'All missiles intercepted', missileAttackIntercepted: 'All missiles intercepted',
@@ -957,11 +1044,13 @@ export default {
missionType: { missionType: {
spy: 'Spy', spy: 'Spy',
attack: 'Attack', attack: 'Attack',
recycle: 'Recycle',
unknown: 'Unknown' unknown: 'Unknown'
}, },
warning: { warning: {
spy: 'Enemy spy incoming!', spy: 'Enemy spy incoming!',
attack: 'Enemy attack incoming!', attack: 'Enemy attack incoming!',
recycle: 'Enemy is recycling debris near you!',
unknown: 'Enemy fleet incoming!' unknown: 'Enemy fleet incoming!'
} }
}, },
@@ -1376,6 +1465,10 @@ export default {
title: 'Battle Simulator', title: 'Battle Simulator',
message: 'Simulate battle outcomes before attacking. Enter both fleets and tech levels to predict victory, losses, and loot.' 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: { settings: {
title: 'Settings', title: 'Settings',
message: 'Manage game data, adjust notifications, export/import saves here. Remember to backup your progress regularly!' message: 'Manage game data, adjust notifications, export/import saves here. Remember to backup your progress regularly!'
@@ -1385,5 +1478,102 @@ export default {
message: message:
'GM mode allows quick modification of resources, buildings, and tech levels. Use it for testing or experiencing full game content.' '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: '士官', officers: '士官',
simulator: 'シミュレーター', simulator: 'シミュレーター',
galaxy: '銀河', galaxy: '銀河',
diplomacy: 'Diplomacy', diplomacy: '外交',
achievements: '実績',
messages: 'メッセージ', messages: 'メッセージ',
settings: '設定', settings: '設定',
guide: 'ゲームガイド',
gm: 'GM' gm: 'GM'
}, },
sidebar: { sidebar: {
@@ -109,6 +111,14 @@ export default {
hour: '時間', hour: '時間',
noEnergy: 'エネルギー不足' noEnergy: 'エネルギー不足'
}, },
energy: {
lowWarning: 'エネルギー不足!資源生産が停止しています!',
severeWarning: 'エネルギー不足!資源生産が停止しています!',
criticalWarning: 'エネルギー不足!資源生産が停止しています!',
noProduction: 'エネルギー不足!資源生産が停止しています!',
deficitDetail: 'エネルギー不足: {deficit}、発電所を建設してください',
buildSolarPlant: '発電所を建設'
},
planet: { planet: {
planet: '惑星', planet: '惑星',
moon: '月', moon: '月',
@@ -434,6 +444,7 @@ export default {
shipyardView: { shipyardView: {
title: '造船所', title: '造船所',
fleetStorage: '艦隊ストレージ', fleetStorage: '艦隊ストレージ',
owned: '所有',
attack: '攻撃力', attack: '攻撃力',
missileAttack: 'ミサイル攻撃', missileAttack: 'ミサイル攻撃',
shield: 'シールド', shield: 'シールド',
@@ -484,8 +495,10 @@ export default {
available: '利用可能', available: '利用可能',
all: '全て', all: '全て',
targetCoordinates: '目標座標', targetCoordinates: '目標座標',
targetType: '目標タイプ',
planet: '惑星',
moon: '月',
galaxy: '銀河', galaxy: '銀河',
diplomacy: 'Diplomacy',
system: '星系', system: '星系',
position: '位置', position: '位置',
missionType: 'ミッションタイプ', missionType: 'ミッションタイプ',
@@ -529,7 +542,30 @@ export default {
noDeathstar: '破壊ミッションにはデススターが必要です', noDeathstar: '破壊ミッションにはデススターが必要です',
giftMode: 'ギフトモード', giftMode: 'ギフトモード',
giftModeDescription: '資源を贈り物として送る', 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: { officersView: {
title: '士官', title: '士官',
@@ -569,7 +605,6 @@ export default {
title: '銀河', title: '銀河',
selectCoordinates: '座標選択', selectCoordinates: '座標選択',
galaxy: '銀河', galaxy: '銀河',
diplomacy: 'Diplomacy',
selectGalaxy: '銀河を選択', selectGalaxy: '銀河を選択',
system: '星系', system: '星系',
selectSystem: '星系を選択', selectSystem: '星系を選択',
@@ -726,14 +761,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: '輸送ミッションが正常に完了しました', transportSuccess: '輸送ミッションが正常に完了しました',
transportFailed: '輸送ミッションが失敗しました', transportFailed: '輸送ミッションが失敗しました',
transportFailedTargetNotFound: '輸送失敗:目標惑星が存在しません',
transportFailedGiftRejected: '輸送失敗:贈り物が拒否されました',
colonizeSuccess: '植民ミッション成功、新しい惑星が確立されました', colonizeSuccess: '植民ミッション成功、新しい惑星が確立されました',
colonizeFailed: '植民ミッションが失敗しました', colonizeFailed: '植民ミッションが失敗しました',
colonizeFailedOccupied: '植民失敗:目標位置は既に他の惑星に占有されています',
colonizeFailedMaxColonies: '植民失敗:コロニー数の上限に達しました。天体物理学を研究して上限を増やしてください。',
spySuccess: 'スパイミッションが正常に完了しました',
spyFailed: 'スパイミッションが失敗しました',
spyFailedTargetNotFound: 'スパイ失敗:目標惑星が存在しません',
deploySuccess: '配備ミッションが正常に完了しました', deploySuccess: '配備ミッションが正常に完了しました',
deployFailed: '配備ミッションが失敗しました', deployFailed: '配備ミッションが失敗しました',
deployFailedTargetNotFound: '配備失敗:目標惑星が存在しません',
deployFailedNotOwnPlanet: '配備失敗:目標惑星はあなたのものではありません',
recycleSuccess: '回収ミッションが正常に完了しました', recycleSuccess: '回収ミッションが正常に完了しました',
recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません', recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません',
recycleFailedNoDebris: '回収失敗:目標位置にデブリフィールドがありません',
recycleFailedDebrisEmpty: '回収失敗:デブリフィールドは既に空です',
destroySuccess: '惑星破壊ミッションが正常に実行されました', destroySuccess: '惑星破壊ミッションが正常に実行されました',
destroyFailed: '惑星破壊ミッションが失敗しました', destroyFailed: '惑星破壊ミッションが失敗しました',
destroyFailedTargetNotFound: '破壊失敗:目標惑星が存在しません',
destroyFailedOwnPlanet: '破壊失敗:自分の惑星を破壊することはできません',
destroyFailedNoDeathstar: '破壊失敗:ミッションを実行するデススターがありません',
destroyFailedChance: '破壊失敗:確率判定に失敗しました(成功率:{chance}%',
missileAttackSuccess: 'ミサイル攻撃成功', missileAttackSuccess: 'ミサイル攻撃成功',
missileAttackFailed: 'ミサイル攻撃失敗、目標惑星が存在しません', missileAttackFailed: 'ミサイル攻撃失敗、目標惑星が存在しません',
missileAttackIntercepted: '全てのミサイルが迎撃されました', missileAttackIntercepted: '全てのミサイルが迎撃されました',
@@ -946,11 +996,13 @@ export default {
missionType: { missionType: {
spy: '偵察', spy: '偵察',
attack: '攻撃', attack: '攻撃',
recycle: '回収',
unknown: '不明' unknown: '不明'
}, },
warning: { warning: {
spy: '敵の偵察が接近中!', spy: '敵の偵察が接近中!',
attack: '敵の攻撃が接近中!', attack: '敵の攻撃が接近中!',
recycle: '敵があなたの近くでデブリを回収中!',
unknown: '敵艦隊が接近中!' unknown: '敵艦隊が接近中!'
} }
}, },
@@ -1238,6 +1290,10 @@ export default {
title: '戦闘シミュレーター', title: '戦闘シミュレーター',
message: '攻撃前に戦闘結果をシミュレート。双方の艦隊と技術レベルを入力して、勝敗と損失を予測。' message: '攻撃前に戦闘結果をシミュレート。双方の艦隊と技術レベルを入力して、勝敗と損失を予測。'
}, },
achievements: {
title: '実績システム',
message: 'ゲーム目標を達成して実績をアンロックし、ダークマター報酬を獲得!実績には複数のティアがあり、高い目標に挑戦してより良い報酬を手に入れましょう。'
},
settings: { settings: {
title: '設定', title: '設定',
message: 'ここでゲームデータの管理、通知設定、セーブのエクスポート/インポートができます。定期的にバックアップを!' message: 'ここでゲームデータの管理、通知設定、セーブのエクスポート/インポートができます。定期的にバックアップを!'
@@ -1246,5 +1302,102 @@ export default {
title: 'GM管理パネル', title: 'GM管理パネル',
message: '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: '장교', officers: '장교',
simulator: '시뮬레이터', simulator: '시뮬레이터',
galaxy: '은하계', galaxy: '은하계',
diplomacy: 'Diplomacy', diplomacy: '외교',
achievements: '업적',
messages: '메시지', messages: '메시지',
settings: '설정', settings: '설정',
gm: 'GM' gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: '시간', hour: '시간',
noEnergy: '에너지 부족' noEnergy: '에너지 부족'
}, },
energy: {
lowWarning: '에너지 부족! 자원 생산 중단!',
severeWarning: '에너지 부족! 자원 생산 중단!',
criticalWarning: '에너지 부족! 자원 생산 중단!',
noProduction: '에너지 부족! 자원 생산 중단!',
deficitDetail: '에너지 부족: {deficit}, 발전소를 더 건설하세요',
buildSolarPlant: '발전소 건설'
},
planet: { planet: {
planet: '행성', planet: '행성',
moon: '위성', moon: '위성',
@@ -420,6 +429,7 @@ export default {
shipyardView: { shipyardView: {
title: '조선소', title: '조선소',
fleetStorage: '함대 저장소', fleetStorage: '함대 저장소',
owned: '보유',
attack: '공격력', attack: '공격력',
missileAttack: '미사일 공격', missileAttack: '미사일 공격',
shield: '실드', shield: '실드',
@@ -484,8 +494,10 @@ export default {
available: '사용 가능', available: '사용 가능',
all: '전체', all: '전체',
targetCoordinates: '목표 좌표', targetCoordinates: '목표 좌표',
targetType: '목표 유형',
planet: '행성',
moon: '달',
galaxy: '은하계', galaxy: '은하계',
diplomacy: 'Diplomacy',
system: '행성계', system: '행성계',
position: '위치', position: '위치',
missionType: '임무 유형', missionType: '임무 유형',
@@ -529,7 +541,30 @@ export default {
noDeathstar: '파괴 임무를 위해 데스스타가 필요합니다', noDeathstar: '파괴 임무를 위해 데스스타가 필요합니다',
giftMode: '선물 모드', giftMode: '선물 모드',
giftModeDescription: '자원을 선물로 보내기', 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: { officersView: {
title: '장교', title: '장교',
@@ -569,7 +604,6 @@ export default {
title: '은하계', title: '은하계',
selectCoordinates: '좌표 선택', selectCoordinates: '좌표 선택',
galaxy: '은하계', galaxy: '은하계',
diplomacy: 'Diplomacy',
selectGalaxy: '은하계 선택', selectGalaxy: '은하계 선택',
system: '행성계', system: '행성계',
selectSystem: '행성계 선택', selectSystem: '행성계 선택',
@@ -728,14 +762,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: '수송 임무가 성공적으로 완료되었습니다', transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
transportFailed: '수송 임무 실패', transportFailed: '수송 임무 실패',
transportFailedTargetNotFound: '수송 실패: 목표 행성이 존재하지 않습니다',
transportFailedGiftRejected: '수송 실패: 선물이 거절되었습니다',
colonizeSuccess: '식민 임무 성공, 새로운 행성이 건설되었습니다', colonizeSuccess: '식민 임무 성공, 새로운 행성이 건설되었습니다',
colonizeFailed: '식민 임무 실패', colonizeFailed: '식민 임무 실패',
colonizeFailedOccupied: '식민 실패: 목표 위치가 이미 다른 행성에 의해 점유되어 있습니다',
colonizeFailedMaxColonies: '식민 실패: 최대 식민지 수에 도달했습니다. 천체물리학을 연구하여 한도를 늘리세요.',
spySuccess: '정찰 임무가 성공적으로 완료되었습니다',
spyFailed: '정찰 임무 실패',
spyFailedTargetNotFound: '정찰 실패: 목표 행성이 존재하지 않습니다',
deploySuccess: '배치 임무가 성공적으로 완료되었습니다', deploySuccess: '배치 임무가 성공적으로 완료되었습니다',
deployFailed: '배치 임무 실패', deployFailed: '배치 임무 실패',
deployFailedTargetNotFound: '배치 실패: 목표 행성이 존재하지 않습니다',
deployFailedNotOwnPlanet: '배치 실패: 목표 행성이 당신의 것이 아닙니다',
recycleSuccess: '회수 임무가 성공적으로 완료되었습니다', recycleSuccess: '회수 임무가 성공적으로 완료되었습니다',
recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다', recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다',
recycleFailedNoDebris: '회수 실패: 목표 위치에 잔해장이 없습니다',
recycleFailedDebrisEmpty: '회수 실패: 잔해장이 이미 비어 있습니다',
destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다', destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다',
destroyFailed: '행성 파괴 임무 실패', destroyFailed: '행성 파괴 임무 실패',
destroyFailedTargetNotFound: '파괴 실패: 목표 행성이 존재하지 않습니다',
destroyFailedOwnPlanet: '파괴 실패: 자신의 행성을 파괴할 수 없습니다',
destroyFailedNoDeathstar: '파괴 실패: 임무를 수행할 데스스타가 없습니다',
destroyFailedChance: '파괴 실패: 확률 판정 실패 (성공률: {chance}%)',
missileAttackSuccess: '미사일 공격 성공', missileAttackSuccess: '미사일 공격 성공',
missileAttackFailed: '미사일 공격 실패, 목표 행성이 존재하지 않습니다', missileAttackFailed: '미사일 공격 실패, 목표 행성이 존재하지 않습니다',
missileAttackIntercepted: '모든 미사일이 요격되었습니다', missileAttackIntercepted: '모든 미사일이 요격되었습니다',
@@ -948,11 +997,13 @@ export default {
missionType: { missionType: {
spy: '정찰', spy: '정찰',
attack: '공격', attack: '공격',
recycle: '회수',
unknown: '알 수 없음' unknown: '알 수 없음'
}, },
warning: { warning: {
spy: '적 정찰 접근 중!', spy: '적 정찰 접근 중!',
attack: '적 공격 접근 중!', attack: '적 공격 접근 중!',
recycle: '적이 근처에서 잔해를 회수 중!',
unknown: '적 함대 접근 중!' unknown: '적 함대 접근 중!'
} }
}, },
@@ -1240,6 +1291,10 @@ export default {
title: '전투 시뮬레이터', title: '전투 시뮬레이터',
message: '공격 전에 전투 결과를 시뮬레이션하세요. 양측 함대와 기술 레벨을 입력하여 승패와 손실을 예측.' message: '공격 전에 전투 결과를 시뮬레이션하세요. 양측 함대와 기술 레벨을 입력하여 승패와 손실을 예측.'
}, },
achievements: {
title: '업적 시스템',
message: '게임 목표를 완료하여 업적을 해제하고 암흑 물질 보상을 획득하세요! 업적은 여러 등급이 있으며, 더 높은 도전으로 더 좋은 보상을 받으세요.'
},
settings: { settings: {
title: '설정', title: '설정',
message: '여기서 게임 데이터 관리, 알림 설정, 저장 내보내기/가져오기가 가능합니다. 정기적으로 백업하세요!' message: '여기서 게임 데이터 관리, 알림 설정, 저장 내보내기/가져오기가 가능합니다. 정기적으로 백업하세요!'
@@ -1248,5 +1303,102 @@ export default {
title: 'GM 관리 패널', title: 'GM 관리 패널',
message: '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: 'Офицеры', officers: 'Офицеры',
simulator: 'Симулятор', simulator: 'Симулятор',
galaxy: 'Галактика', galaxy: 'Галактика',
diplomacy: 'Diplomacy', diplomacy: 'Дипломатия',
achievements: 'Достижения',
messages: 'Сообщения', messages: 'Сообщения',
settings: 'Настройки', settings: 'Настройки',
gm: 'GM' gm: 'GM'
@@ -109,6 +110,14 @@ export default {
hour: 'час', hour: 'час',
noEnergy: 'Нет энергии' noEnergy: 'Нет энергии'
}, },
energy: {
lowWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
severeWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
criticalWarning: 'Дефицит энергии! Производство ресурсов остановлено!',
noProduction: 'Дефицит энергии! Производство ресурсов остановлено!',
deficitDetail: 'Дефицит энергии: {deficit}, постройте больше электростанций',
buildSolarPlant: 'Построить электростанцию'
},
planet: { planet: {
planet: 'Планета', planet: 'Планета',
moon: 'Луна', moon: 'Луна',
@@ -422,6 +431,7 @@ export default {
shipyardView: { shipyardView: {
title: 'Верфь', title: 'Верфь',
fleetStorage: 'Хранилище флота', fleetStorage: 'Хранилище флота',
owned: 'В наличии',
attack: 'Атака', attack: 'Атака',
missileAttack: 'Ракетная атака', missileAttack: 'Ракетная атака',
shield: 'Щит', shield: 'Щит',
@@ -487,8 +497,10 @@ export default {
available: 'Доступно', available: 'Доступно',
all: 'Все', all: 'Все',
targetCoordinates: 'Целевые координаты', targetCoordinates: 'Целевые координаты',
targetType: 'Тип цели',
planet: 'Планета',
moon: 'Луна',
galaxy: 'Галактика', galaxy: 'Галактика',
diplomacy: 'Diplomacy',
system: 'Система', system: 'Система',
position: 'Позиция', position: 'Позиция',
missionType: 'Тип миссии', missionType: 'Тип миссии',
@@ -532,7 +544,30 @@ export default {
noDeathstar: 'Для миссии разрушения требуется Звезда Смерти', noDeathstar: 'Для миссии разрушения требуется Звезда Смерти',
giftMode: 'Режим подарка', giftMode: 'Режим подарка',
giftModeDescription: 'Отправить ресурсы в подарок', 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: { officersView: {
title: 'Офицеры', title: 'Офицеры',
@@ -572,7 +607,6 @@ export default {
title: 'Галактика', title: 'Галактика',
selectCoordinates: 'Выбрать координаты', selectCoordinates: 'Выбрать координаты',
galaxy: 'Галактика', galaxy: 'Галактика',
diplomacy: 'Diplomacy',
selectGalaxy: 'Выбрать галактику', selectGalaxy: 'Выбрать галактику',
system: 'Система', system: 'Система',
selectSystem: 'Выбрать систему', selectSystem: 'Выбрать систему',
@@ -734,14 +768,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: 'Миссия транспортировки успешно завершена', transportSuccess: 'Миссия транспортировки успешно завершена',
transportFailed: 'Миссия транспортировки провалена', transportFailed: 'Миссия транспортировки провалена',
transportFailedTargetNotFound: 'Транспортировка провалена: Целевая планета не существует',
transportFailedGiftRejected: 'Транспортировка провалена: Подарок был отклонён',
colonizeSuccess: 'Миссия колонизации успешна, новая планета создана', colonizeSuccess: 'Миссия колонизации успешна, новая планета создана',
colonizeFailed: 'Миссия колонизации провалена', colonizeFailed: 'Миссия колонизации провалена',
colonizeFailedOccupied: 'Колонизация провалена: Целевая позиция уже занята другой планетой',
colonizeFailedMaxColonies: 'Колонизация провалена: Достигнуто максимальное количество колоний. Исследуйте Астрофизику для увеличения лимита.',
spySuccess: 'Миссия шпионажа успешно завершена',
spyFailed: 'Миссия шпионажа провалена',
spyFailedTargetNotFound: 'Шпионаж провален: Целевая планета не существует',
deploySuccess: 'Миссия размещения успешно завершена', deploySuccess: 'Миссия размещения успешно завершена',
deployFailed: 'Миссия размещения провалена', deployFailed: 'Миссия размещения провалена',
deployFailedTargetNotFound: 'Размещение провалено: Целевая планета не существует',
deployFailedNotOwnPlanet: 'Размещение провалено: Целевая планета не принадлежит вам',
recycleSuccess: 'Миссия переработки успешно завершена', recycleSuccess: 'Миссия переработки успешно завершена',
recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции', recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции',
recycleFailedNoDebris: 'Переработка провалена: Нет поля обломков в целевой позиции',
recycleFailedDebrisEmpty: 'Переработка провалена: Поле обломков уже пусто',
destroySuccess: 'Миссия уничтожения планеты успешно выполнена', destroySuccess: 'Миссия уничтожения планеты успешно выполнена',
destroyFailed: 'Миссия уничтожения планеты провалена', destroyFailed: 'Миссия уничтожения планеты провалена',
destroyFailedTargetNotFound: 'Уничтожение провалено: Целевая планета не существует',
destroyFailedOwnPlanet: 'Уничтожение провалено: Нельзя уничтожить собственную планету',
destroyFailedNoDeathstar: 'Уничтожение провалено: Нет Звезды Смерти для выполнения миссии',
destroyFailedChance: 'Уничтожение провалено: Проверка вероятности не удалась (Шанс успеха: {chance}%)',
expeditionResources: 'Экспедиция обнаружила ресурсы!', expeditionResources: 'Экспедиция обнаружила ресурсы!',
expeditionDarkMatter: 'Экспедиция обнаружила тёмную материю!', expeditionDarkMatter: 'Экспедиция обнаружила тёмную материю!',
expeditionFleet: 'Экспедиция обнаружила заброшенные корабли!', expeditionFleet: 'Экспедиция обнаружила заброшенные корабли!',
@@ -1256,6 +1305,10 @@ export default {
title: 'Симулятор боя', title: 'Симулятор боя',
message: 'Симулируйте результаты боя перед атакой. Введите флоты и уровни технологий для прогноза победы, потерь и добычи.' message: 'Симулируйте результаты боя перед атакой. Введите флоты и уровни технологий для прогноза победы, потерь и добычи.'
}, },
achievements: {
title: 'Система достижений',
message: 'Выполняйте игровые цели для разблокировки достижений и получения наград в виде тёмной материи! Достижения имеют несколько уровней - стремитесь к более высоким целям для лучших наград.'
},
settings: { settings: {
title: 'Настройки', title: 'Настройки',
message: 'Управляйте игровыми данными, уведомлениями, импортом/экспортом сохранений. Регулярно создавайте резервные копии!' message: 'Управляйте игровыми данными, уведомлениями, импортом/экспортом сохранений. Регулярно создавайте резервные копии!'
@@ -1265,5 +1318,102 @@ export default {
message: 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: '模拟', simulator: '模拟',
galaxy: '星系', galaxy: '星系',
diplomacy: '外交', diplomacy: '外交',
achievements: '成就',
messages: '消息', messages: '消息',
settings: '设置', settings: '设置',
gm: 'GM' gm: 'GM'
@@ -108,6 +109,14 @@ export default {
hour: '小时', hour: '小时',
noEnergy: '电力不足' noEnergy: '电力不足'
}, },
energy: {
lowWarning: '电力不足,资源生产已停止!',
severeWarning: '电力不足,资源生产已停止!',
criticalWarning: '电力不足,资源生产已停止!',
noProduction: '电力不足,资源生产已停止!',
deficitDetail: '电力缺口: {deficit},请建造更多电站',
buildSolarPlant: '建造电站'
},
planet: { planet: {
planet: '星球', planet: '星球',
moon: '月球', moon: '月球',
@@ -416,6 +425,7 @@ export default {
shipyardView: { shipyardView: {
title: '船坞', title: '船坞',
fleetStorage: '舰队仓储', fleetStorage: '舰队仓储',
owned: '拥有',
attack: '攻击力', attack: '攻击力',
shield: '护盾', shield: '护盾',
speed: '速度', speed: '速度',
@@ -475,6 +485,9 @@ export default {
available: '可用', available: '可用',
all: '全部', all: '全部',
targetCoordinates: '目标坐标', targetCoordinates: '目标坐标',
targetType: '目标类型',
planet: '行星',
moon: '月球',
galaxy: '银河系', galaxy: '银河系',
system: '星系', system: '星系',
position: '位置', position: '位置',
@@ -520,7 +533,47 @@ export default {
noDeathstar: '需要死星才能执行毁灭任务', noDeathstar: '需要死星才能执行毁灭任务',
giftMode: '赠送模式', giftMode: '赠送模式',
giftModeDescription: '将资源作为礼物赠送给', 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: { officersView: {
title: '军官', title: '军官',
@@ -602,7 +655,26 @@ export default {
colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。', colonizePlanetMessage: '确定要殖民位置 [{coordinates}] 吗?\n\n请前往舰队页面派遣殖民船。',
recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。', recyclePlanetMessage: '确定要回收位置 [{coordinates}] 的残骸吗?\n\n请前往舰队页面派遣回收船。',
giftPlanetMessage: '确定要向星球 [{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: { messagesView: {
title: '消息中心', title: '消息中心',
@@ -718,14 +790,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: '运输任务成功完成', transportSuccess: '运输任务成功完成',
transportFailed: '运输任务失败', transportFailed: '运输任务失败',
transportFailedTargetNotFound: '运输失败:目标星球不存在',
transportFailedGiftRejected: '运输失败:礼物被拒绝',
colonizeSuccess: '殖民任务成功,新星球已建立', colonizeSuccess: '殖民任务成功,新星球已建立',
colonizeFailed: '殖民任务失败', colonizeFailed: '殖民任务失败',
colonizeFailedOccupied: '殖民失败:目标位置已被其他星球占用',
colonizeFailedMaxColonies: '殖民失败:已达到殖民地数量上限。研究天体物理学可增加上限。',
spySuccess: '侦查任务成功完成',
spyFailed: '侦查任务失败',
spyFailedTargetNotFound: '侦查失败:目标星球不存在',
deploySuccess: '部署任务成功完成', deploySuccess: '部署任务成功完成',
deployFailed: '部署任务失败', deployFailed: '部署任务失败',
deployFailedTargetNotFound: '部署失败:目标星球不存在',
deployFailedNotOwnPlanet: '部署失败:目标星球不属于你',
recycleSuccess: '回收任务成功完成', recycleSuccess: '回收任务成功完成',
recycleFailed: '回收任务失败,目标位置没有残骸', recycleFailed: '回收任务失败,目标位置没有残骸',
recycleFailedNoDebris: '回收失败:目标位置没有残骸场',
recycleFailedDebrisEmpty: '回收失败:残骸场已被清空',
destroySuccess: '行星毁灭任务成功执行', destroySuccess: '行星毁灭任务成功执行',
destroyFailed: '行星毁灭任务失败', destroyFailed: '行星毁灭任务失败',
destroyFailedTargetNotFound: '毁灭失败:目标星球不存在',
destroyFailedOwnPlanet: '毁灭失败:无法摧毁自己的星球',
destroyFailedNoDeathstar: '毁灭失败:没有死星执行任务',
destroyFailedChance: '毁灭失败:概率判定失败(成功率:{chance}%',
missileAttackSuccess: '导弹攻击成功', missileAttackSuccess: '导弹攻击成功',
missileAttackFailed: '导弹攻击失败,目标星球不存在', missileAttackFailed: '导弹攻击失败,目标星球不存在',
missileAttackIntercepted: '所有导弹被拦截', missileAttackIntercepted: '所有导弹被拦截',
@@ -943,11 +1030,13 @@ export default {
missionType: { missionType: {
spy: '侦查', spy: '侦查',
attack: '攻击', attack: '攻击',
recycle: '回收',
unknown: '未知' unknown: '未知'
}, },
warning: { warning: {
spy: '敌方侦查即将到达!', spy: '敌方侦查即将到达!',
attack: '敌方攻击即将到达!', attack: '敌方攻击即将到达!',
recycle: '敌方正在回收你附近的残骸!',
unknown: '敌方舰队即将到达!' unknown: '敌方舰队即将到达!'
} }
}, },
@@ -1336,6 +1425,10 @@ export default {
title: '战斗模拟器', title: '战斗模拟器',
message: '在发动攻击前模拟战斗结果。输入双方舰队和科技等级,预测胜负和损失。' message: '在发动攻击前模拟战斗结果。输入双方舰队和科技等级,预测胜负和损失。'
}, },
achievements: {
title: '成就系统',
message: '完成各类游戏目标解锁成就,获得暗物质奖励!成就分为多个等级,挑战更高难度获得更丰厚的奖励。'
},
settings: { settings: {
title: '设置', title: '设置',
message: '在这里管理游戏数据、调整通知设置、导出/导入存档。记得定期备份你的进度!' message: '在这里管理游戏数据、调整通知设置、导出/导入存档。记得定期备份你的进度!'
@@ -1344,5 +1437,102 @@ export default {
title: 'GM管理面板', title: 'GM管理面板',
message: '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: '軍官', officers: '軍官',
simulator: '模擬', simulator: '模擬',
galaxy: '星系', galaxy: '星系',
diplomacy: 'Diplomacy', diplomacy: '外交',
achievements: '成就',
messages: '訊息', messages: '訊息',
settings: '設定', settings: '設定',
guide: '遊戲指南',
gm: 'GM' gm: 'GM'
}, },
sidebar: { sidebar: {
@@ -109,6 +111,14 @@ export default {
hour: '小時', hour: '小時',
noEnergy: '電力不足' noEnergy: '電力不足'
}, },
energy: {
lowWarning: '電力不足,資源生產已停止!',
severeWarning: '電力不足,資源生產已停止!',
criticalWarning: '電力不足,資源生產已停止!',
noProduction: '電力不足,資源生產已停止!',
deficitDetail: '電力缺口: {deficit},請建造更多電站',
buildSolarPlant: '建造電站'
},
planet: { planet: {
planet: '星球', planet: '星球',
moon: '月球', moon: '月球',
@@ -422,6 +432,7 @@ export default {
shipyardView: { shipyardView: {
title: '船塢', title: '船塢',
fleetStorage: '艦隊倉儲', fleetStorage: '艦隊倉儲',
owned: '擁有',
attack: '攻擊力', attack: '攻擊力',
missileAttack: '導彈攻擊', missileAttack: '導彈攻擊',
shield: '護盾', shield: '護盾',
@@ -486,8 +497,10 @@ export default {
available: '可用', available: '可用',
all: '全部', all: '全部',
targetCoordinates: '目標座標', targetCoordinates: '目標座標',
targetType: '目標類型',
planet: '行星',
moon: '月球',
galaxy: '銀河系', galaxy: '銀河系',
diplomacy: 'Diplomacy',
system: '星系', system: '星系',
position: '位置', position: '位置',
missionType: '任務類型', missionType: '任務類型',
@@ -531,7 +544,30 @@ export default {
noDeathstar: '需要死星才能執行毀滅任務', noDeathstar: '需要死星才能執行毀滅任務',
giftMode: '贈送模式', giftMode: '贈送模式',
giftModeDescription: '將資源作為禮物贈送給', 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: { officersView: {
title: '軍官', title: '軍官',
@@ -571,7 +607,6 @@ export default {
title: '星系', title: '星系',
selectCoordinates: '選擇座標', selectCoordinates: '選擇座標',
galaxy: '銀河系', galaxy: '銀河系',
diplomacy: 'Diplomacy',
selectGalaxy: '選擇銀河系', selectGalaxy: '選擇銀河系',
system: '星系', system: '星系',
selectSystem: '選擇星系', selectSystem: '選擇星系',
@@ -728,14 +763,29 @@ export default {
missionReports: { missionReports: {
transportSuccess: '運輸任務成功完成', transportSuccess: '運輸任務成功完成',
transportFailed: '運輸任務失敗', transportFailed: '運輸任務失敗',
transportFailedTargetNotFound: '運輸失敗:目標星球不存在',
transportFailedGiftRejected: '運輸失敗:禮物被拒絕',
colonizeSuccess: '殖民任務成功,新星球已建立', colonizeSuccess: '殖民任務成功,新星球已建立',
colonizeFailed: '殖民任務失敗', colonizeFailed: '殖民任務失敗',
colonizeFailedOccupied: '殖民失敗:目標位置已被其他星球佔用',
colonizeFailedMaxColonies: '殖民失敗:已達到殖民地數量上限。研究天體物理學可增加上限。',
spySuccess: '偵查任務成功完成',
spyFailed: '偵查任務失敗',
spyFailedTargetNotFound: '偵查失敗:目標星球不存在',
deploySuccess: '部署任務成功完成', deploySuccess: '部署任務成功完成',
deployFailed: '部署任務失敗', deployFailed: '部署任務失敗',
deployFailedTargetNotFound: '部署失敗:目標星球不存在',
deployFailedNotOwnPlanet: '部署失敗:目標星球不屬於你',
recycleSuccess: '回收任務成功完成', recycleSuccess: '回收任務成功完成',
recycleFailed: '回收任務失敗,目標位置沒有殘骸', recycleFailed: '回收任務失敗,目標位置沒有殘骸',
recycleFailedNoDebris: '回收失敗:目標位置沒有殘骸場',
recycleFailedDebrisEmpty: '回收失敗:殘骸場已被清空',
destroySuccess: '行星毀滅任務成功執行', destroySuccess: '行星毀滅任務成功執行',
destroyFailed: '行星毀滅任務失敗', destroyFailed: '行星毀滅任務失敗',
destroyFailedTargetNotFound: '毀滅失敗:目標星球不存在',
destroyFailedOwnPlanet: '毀滅失敗:無法摧毀自己的星球',
destroyFailedNoDeathstar: '毀滅失敗:沒有死星執行任務',
destroyFailedChance: '毀滅失敗:概率判定失敗(成功率:{chance}%',
expeditionResources: '探險隊發現了資源!', expeditionResources: '探險隊發現了資源!',
expeditionDarkMatter: '探險隊發現了暗物質!', expeditionDarkMatter: '探險隊發現了暗物質!',
expeditionFleet: '探險隊發現了廢棄的艦船!', expeditionFleet: '探險隊發現了廢棄的艦船!',
@@ -948,11 +998,13 @@ export default {
missionType: { missionType: {
spy: '偵查', spy: '偵查',
attack: '攻擊', attack: '攻擊',
recycle: '回收',
unknown: '未知' unknown: '未知'
}, },
warning: { warning: {
spy: '敵方偵查即將到達!', spy: '敵方偵查即將到達!',
attack: '敵方攻擊即將到達!', attack: '敵方攻擊即將到達!',
recycle: '敵方正在回收你附近的殘骸!',
unknown: '敵方艦隊即將到達!' unknown: '敵方艦隊即將到達!'
} }
}, },
@@ -1235,6 +1287,10 @@ export default {
title: '戰鬥模擬器', title: '戰鬥模擬器',
message: '在發動攻擊前模擬戰鬥結果。輸入雙方艦隊和科技等級,預測勝負和損失。' message: '在發動攻擊前模擬戰鬥結果。輸入雙方艦隊和科技等級,預測勝負和損失。'
}, },
achievements: {
title: '成就系統',
message: '完成各類遊戲目標解鎖成就,獲得暗物質獎勵!成就分為多個等級,挑戰更高難度獲得更豐厚的獎勵。'
},
settings: { settings: {
title: '設置', title: '設置',
message: '在這裡管理遊戲數據、調整通知設置、導出/導入存檔。記得定期備份你的進度!' message: '在這裡管理遊戲數據、調整通知設置、導出/導入存檔。記得定期備份你的進度!'
@@ -1243,5 +1299,239 @@ export default {
title: 'GM管理面板', title: 'GM管理面板',
message: '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[], allNpcs?: NPC[],
locale: Locale = 'zh-CN', locale: Locale = 'zh-CN',
storageCapacityBonus: number = 0 storageCapacityBonus: number = 0
): { success: boolean; reputationGain?: number; overflow?: Resources } => { ): TransportResult => {
// 检查是否是赠送任务 // 检查是否是赠送任务
if (mission.isGift && mission.giftTargetNpcId && player && allNpcs) { if (mission.isGift && mission.giftTargetNpcId && player && allNpcs) {
const targetNpc = allNpcs.find(n => n.id === mission.giftTargetNpcId) const targetNpc = allNpcs.find(n => n.id === mission.giftTargetNpcId)
@@ -94,7 +109,7 @@ export const processTransportArrival = (
// 如果礼物被拒绝,资源返还给玩家 // 如果礼物被拒绝,资源返还给玩家
if (!giftResult.accepted) { if (!giftResult.accepted) {
// 资源保留在cargo中返回时会退还给玩家 // 资源保留在cargo中返回时会退还给玩家
return { success: false, reputationGain: undefined } return { success: false, reputationGain: undefined, failReason: 'giftRejected' }
} }
// 礼物被接受,清空货物 // 礼物被接受,清空货物
@@ -114,10 +129,13 @@ export const processTransportArrival = (
mission.cargo = result.overflow mission.cargo = result.overflow
return { success: true, overflow: 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.status = 'returning'
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 } return { success: false, failReason: 'targetNotFound' }
return { success: false }
} }
/** /**
@@ -356,6 +374,15 @@ export const canColonize = (player: Player): boolean => {
return currentPlanets < maxPlanets return currentPlanets < maxPlanets
} }
/**
* 殖民任务结果
*/
export interface ColonizeResult {
success: boolean
planet: Planet | null
failReason?: 'positionOccupied' | 'maxColoniesReached'
}
/** /**
* 处理殖民任务到达 * 处理殖民任务到达
*/ */
@@ -364,18 +391,18 @@ export const processColonizeArrival = (
targetPlanet: Planet | undefined, targetPlanet: Planet | undefined,
player: Player, player: Player,
colonyNameTemplate: string = 'Colony' colonyNameTemplate: string = 'Colony'
): Planet | null => { ): ColonizeResult => {
if (targetPlanet) { if (targetPlanet) {
// 位置已被占用 // 位置已被占用
mission.status = 'returning' mission.status = 'returning'
return null return { success: false, planet: null, failReason: 'positionOccupied' }
} }
// 检查殖民地槽位限制 // 检查殖民地槽位限制
if (!canColonize(player)) { if (!canColonize(player)) {
// 超出殖民地数量限制,殖民船返回 // 超出殖民地数量限制,殖民船返回
mission.status = 'returning' 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.fleet[ShipType.ColonyShip] = (mission.fleet[ShipType.ColonyShip] || 1) - 1
mission.status = 'returning' 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)) 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, defender: Player | null,
allNpcs?: NPC[], allNpcs?: NPC[],
locale: Locale = 'zh-CN' locale: Locale = 'zh-CN'
): SpyReport | null => { ): SpyResult => {
if (!targetPlanet) { if (!targetPlanet) {
mission.status = 'returning' mission.status = 'returning'
return null return { success: false, failReason: 'targetNotFound' }
} }
// 获取间谍技术等级 // 获取间谍技术等级
@@ -536,7 +577,21 @@ export const processSpyArrival = (
} }
mission.status = 'returning' 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, targetPlanet: Planet | undefined,
playerId: string, playerId: string,
technologies: Record<TechnologyType, number> technologies: Record<TechnologyType, number>
): { success: boolean; overflow?: Partial<Record<ShipType, number>> } => { ): DeployResult => {
if (!targetPlanet || targetPlanet.ownerId !== playerId) { if (!targetPlanet) {
mission.status = 'returning' 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 } 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, player?: Player,
allNpcs?: NPC[], allNpcs?: NPC[],
locale: Locale = 'zh-CN' locale: Locale = 'zh-CN'
): { collectedResources: Pick<Resources, 'metal' | 'crystal'>; remainingDebris: Pick<Resources, 'metal' | 'crystal'> | null } | null => { ): RecycleResult => {
if (!debrisField) { if (!debrisField) {
mission.status = 'returning' mission.status = 'returning'
return null return { success: false, failReason: 'noDebrisField' }
} }
// 计算回收船的货舱容量 // 计算回收船的货舱容量
@@ -599,7 +673,7 @@ export const processRecycleArrival = (
// 防止除零如果残骸为0直接返回 // 防止除零如果残骸为0直接返回
if (totalDebris === 0) { if (totalDebris === 0) {
mission.status = 'returning' mission.status = 'returning'
return null return { success: false, failReason: 'debrisEmpty' }
} }
// 按比例收集金属和晶体 // 按比例收集金属和晶体
@@ -625,6 +699,7 @@ export const processRecycleArrival = (
} }
return { return {
success: true,
collectedResources: { collectedResources: {
metal: collectedMetal, metal: collectedMetal,
crystal: collectedCrystal 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 = ( export const processDestroyArrival = (
mission: FleetMission, mission: FleetMission,
targetPlanet: Planet | undefined, targetPlanet: Planet | undefined,
attacker: Player attacker: Player
): { success: boolean; destructionChance: number; planetId?: string } | null => { ): DestroyResult => {
if (!targetPlanet || targetPlanet.ownerId === attacker.id) { if (!targetPlanet) {
mission.status = 'returning' 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 const deathstarCount = mission.fleet[ShipType.Deathstar] || 0
if (deathstarCount === 0) { if (deathstarCount === 0) {
mission.status = 'returning' 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 if (targetPlanet.isMoon) {
const defensePower = calculatePlanetDefensePower(targetPlanet.fleet, targetPlanet.defense) // 月球销毁使用 OGame 公式
const result = moonLogic.tryDestroyMoon(targetPlanet, deathstarCount)
// 计算摧毁概率 // 如果死星被反向销毁,从任务舰队中移除
const destructionChance = calculateDestructionChance(deathstarCount, planetaryShieldCount, defensePower) if (result.deathstarsDestroyed) {
mission.fleet[ShipType.Deathstar] = 0
}
// 随机判断是否成功 mission.status = 'returning'
const randomValue = Math.random() * 100
const success = randomValue < destructionChance
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 { const randomValue = Math.random() * 100
success, const success = randomValue < destructionChance
destructionChance,
planetId: success ? targetPlanet.id : undefined 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: case MissionType.Colonize:
const newColony = processColonizeArrival(mission, targetPlanet, attacker) const colonizeResult = processColonizeArrival(mission, targetPlanet, attacker)
if (newColony) { if (colonizeResult.success && colonizeResult.planet) {
newColonies.push(newColony) newColonies.push(colonizeResult.planet)
planets.set(targetKey, newColony) planets.set(targetKey, colonizeResult.planet)
} }
break break
case MissionType.Spy: case MissionType.Spy:
const spyReport = processSpyArrival(mission, targetPlanet, attacker, defender) const spyResult = processSpyArrival(mission, targetPlanet, attacker, defender)
if (spyReport) { if (spyResult.success && spyResult.report) {
spyReports.push(spyReport) spyReports.push(spyResult.report)
} }
break break
@@ -1010,7 +1157,7 @@ export const updateFleetMissions = async (
const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}` const debrisId = `debris_${mission.targetPosition.galaxy}_${mission.targetPosition.system}_${mission.targetPosition.position}`
const debrisField = debrisFields.get(debrisId) const debrisField = debrisFields.get(debrisId)
const recycleResult = processRecycleArrival(mission, debrisField, attacker, allNpcs) const recycleResult = processRecycleArrival(mission, debrisField, attacker, allNpcs)
if (recycleResult) { if (recycleResult.success && recycleResult.collectedResources) {
if (recycleResult.remainingDebris) { if (recycleResult.remainingDebris) {
// 更新残骸场 // 更新残骸场
const updatedDebris: DebrisField = { const updatedDebris: DebrisField = {
@@ -1029,7 +1176,7 @@ export const updateFleetMissions = async (
case MissionType.Destroy: case MissionType.Destroy:
const destroyResult = processDestroyArrival(mission, targetPlanet, attacker) const destroyResult = processDestroyArrival(mission, targetPlanet, attacker)
if (destroyResult && destroyResult.success && destroyResult.planetId) { if (destroyResult.success && destroyResult.planetId) {
// 星球被摧毁 // 星球被摧毁
destroyedPlanetIds.push(destroyResult.planetId) destroyedPlanetIds.push(destroyResult.planetId)

View File

@@ -6,6 +6,7 @@ import * as researchLogic from './researchLogic'
import * as pointsLogic from './pointsLogic' import * as pointsLogic from './pointsLogic'
import * as planetLogic from './planetLogic' import * as planetLogic from './planetLogic'
import * as resourceLogic from './resourceLogic' import * as resourceLogic from './resourceLogic'
import * as achievementLogic from './achievementLogic'
/** /**
* 初始化玩家数据 * 初始化玩家数据
@@ -105,6 +106,14 @@ export const processGameUpdate = (
): { ): {
updatedResearchQueue: BuildQueueItem[] updatedResearchQueue: BuildQueueItem[]
} => { } => {
// 确保成就统计数据存在
if (!player.achievementStats) {
player.achievementStats = achievementLogic.initializeAchievementStats()
}
if (!player.achievements) {
player.achievements = achievementLogic.initializeAchievements()
}
// 获取军官加成 // 获取军官加成
const bonuses = officerLogic.calculateActiveBonuses(player.officers, now) const bonuses = officerLogic.calculateActiveBonuses(player.officers, now)
@@ -113,16 +122,48 @@ export const processGameUpdate = (
pointsLogic.addPoints(player, points) 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) { if (onNotification) {
onNotification(type, itemType, level) 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 通信开销) // 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
player.planets.forEach(planet => { player.planets.forEach(planet => {
// 计算更新前的资源(用于计算生产量)
const resourcesBefore = { ...planet.resources }
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed) 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 => { export const checkOfficersExpiration = (officers: Record<OfficerType, Officer>, now: number): void => {
officerLogic.checkAndDeactivateExpiredOfficers(officers, now) 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 战斗产生的残骸场 * @param debrisField 战斗产生的残骸场
* @returns 生成概率0-100 * @returns 生成概率0-20
*/ */
export const calculateMoonGenerationChance = (debrisField: Resources): number => { export const calculateMoonGenerationChance = (debrisField: Resources): number => {
const totalDebris = debrisField.metal + debrisField.crystal const totalDebris = debrisField.metal + debrisField.crystal
@@ -15,18 +16,42 @@ export const calculateMoonGenerationChance = (debrisField: Resources): number =>
return 0 return 0
} }
// 计算概率:基础概率 + (残骸量 / 每单位增加量) * 1% // 计算概率:每100k残骸 = 1%
const additionalChance = Math.floor(totalDebris / MOON_CONFIG.chancePerDebris) const chance = Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)
const chance = MOON_CONFIG.baseChance + additionalChance
// 限制在最大概率内 // 限制在最大概率内20%
return Math.min(chance, MOON_CONFIG.maxChance) 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 debrisField 战斗产生的残骸场
* @param planetPosition 星球坐标 * @param planetPosition 星球坐标
* @param planetId 母星ID
* @param playerId 玩家ID * @param playerId 玩家ID
* @returns 生成的月球对象如果未生成则返回null * @returns 生成的月球对象如果未生成则返回null
*/ */
@@ -49,6 +74,9 @@ export const tryGenerateMoon = (
return null return null
} }
// 计算月球直径
const diameter = calculateMoonDiameter(chance)
// 生成月球 // 生成月球
const moon: Planet = { const moon: Planet = {
id: `moon_${Date.now()}`, id: `moon_${Date.now()}`,
@@ -89,10 +117,11 @@ export const tryGenerateMoon = (
}, },
buildQueue: [], buildQueue: [],
lastUpdate: Date.now(), lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize, maxSpace: MOON_CONFIG.baseFields, // OGame规则月球初始只有1格空间
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage, maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true, isMoon: true,
parentPlanetId: planetId parentPlanetId: planetId,
diameter: diameter // 月球直径(km)
} }
// 初始化所有建筑为0级 // 初始化所有建筑为0级
@@ -118,3 +147,182 @@ export const hasMoonAtPosition = (planets: Planet[], position: { galaxy: number;
p.position.position === position.position 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 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 { BUILDINGS, SHIPS, TECHNOLOGIES } from '@/config/gameConfig'
import * as buildingLogic from './buildingLogic' import * as buildingLogic from './buildingLogic'
import * as researchLogic from './researchLogic' 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 = ( export const createMoon = (
parentPlanet: Planet, parentPlanet: Planet,
position: { galaxy: number; system: number; position: number }, position: { galaxy: number; system: number; position: number },
playerId: string, playerId: string,
moonSuffix: string = "'s Moon" moonSuffix: string = "'s Moon",
diameter?: number
): Planet => { ): Planet => {
const moonId = `moon_${Date.now()}` const moonId = `moon_${Date.now()}`
const moon: Planet = { const moon: Planet = {
@@ -196,10 +202,11 @@ export const createMoon = (
}, },
buildQueue: [], buildQueue: [],
lastUpdate: Date.now(), lastUpdate: Date.now(),
maxSpace: MOON_CONFIG.baseSize, maxSpace: MOON_CONFIG.baseFields, // OGame规则月球初始只有1格空间
maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage, maxFleetStorage: FLEET_STORAGE_CONFIG.baseStorage,
isMoon: true, 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 => { export const calculateMoonMaxSpace = (moon: Planet): number => {
if (!moon.isMoon) return 0 if (!moon.isMoon) return 0
const lunarBaseLevel = moon.buildings[BuildingType.LunarBase] || 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 planet 星球对象
* @param additionalBuildQueue 军官提供的额外队列数量 * @param additionalBuildQueue 军官提供的额外队列数量
* @returns 最大建造队列数量(基础1个 + 纳米工厂等级 + 军官加成最多10个 * @returns 最大建造队列数量(基础3个 + 纳米工厂等级 + 军官加成最多10个
*/ */
export const getMaxBuildQueue = (planet: Planet, additionalBuildQueue: number = 0): number => { export const getMaxBuildQueue = (planet: Planet, additionalBuildQueue: number = 0): number => {
const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0 const naniteFactoryLevel = planet.buildings[BuildingType.NaniteFactory] || 0
return Math.min(1 + naniteFactoryLevel + additionalBuildQueue, 10) return Math.min(3 + naniteFactoryLevel + additionalBuildQueue, 10)
} }
/** /**
* 计算最大研究队列数量 * 计算最大研究队列数量
* @param technologies 已研究的科技等级 * @param technologies 已研究的科技等级
* @returns 最大研究队列数量(基础1个 + 计算机技术等级最多10个 * @returns 最大研究队列数量(基础3个 + 计算机技术等级最多10个
*/ */
export const getMaxResearchQueue = (technologies: Partial<Record<TechnologyType, number>>): number => { export const getMaxResearchQueue = (technologies: Partial<Record<TechnologyType, number>>): number => {
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0 const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
return Math.min(1 + computerTechLevel, 10) return Math.min(3 + computerTechLevel, 10)
} }
/** /**
* 计算最大舰队任务数量 * 计算最大舰队任务数量
* @param additionalFleetSlots 军官提供的额外槽位数量 * @param additionalFleetSlots 军官提供的额外槽位数量
* @param computerTechnologyLevel 计算机技术等级 * @param computerTechnologyLevel 计算机技术等级
* @returns 最大舰队任务数量(基础1个 + 计算机技术等级 + 军官加成最多20个 * @returns 最大舰队任务数量(基础3个 + 计算机技术等级 + 军官加成最多20个
*/ */
export const getMaxFleetMissions = (additionalFleetSlots: number = 0, computerTechnologyLevel: number = 0): number => { 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 const bonus = 1 + (storageCapacityBonus || 0) / 100
// OGame规则基础容量10000资源可以超过容量只影响生产不会丢失
// 月球没有矿场,所以超过容量没有影响,玩家可以从行星运输资源到月球
const baseCapacity = 10000 const baseCapacity = 10000
const darkMatterBaseCapacity = 1000 // 暗物质基础容量较小 const darkMatterBaseCapacity = 1000 // 暗物质基础容量较小
return { return {

View File

@@ -16,6 +16,7 @@ const router = createRouter({
{ path: '/messages', name: 'messages', component: () => import('@/views/MessagesView.vue') }, { path: '/messages', name: 'messages', component: () => import('@/views/MessagesView.vue') },
{ path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') }, { path: '/galaxy', name: 'galaxy', component: () => import('@/views/GalaxyView.vue') },
{ path: '/diplomacy', name: 'diplomacy', component: () => import('@/views/DiplomacyView.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: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
{ path: '/gm', name: 'gm', component: () => import('@/views/GMView.vue') }, { path: '/gm', name: 'gm', component: () => import('@/views/GMView.vue') },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue') } { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue') }

View File

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

View File

@@ -267,6 +267,16 @@ export interface DiplomaticReport {
read?: boolean // 已读状态 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 { export interface FleetMission {
id: string id: string
@@ -275,6 +285,7 @@ export interface FleetMission {
isHostile?: boolean // 是否是敌对任务(用于警告显示) isHostile?: boolean // 是否是敌对任务(用于警告显示)
originPlanetId: string originPlanetId: string
targetPosition: { galaxy: number; system: number; position: number } targetPosition: { galaxy: number; system: number; position: number }
targetIsMoon?: boolean // 目标是否为月球(用于区分同坐标的行星和月球)
targetPlanetId?: string targetPlanetId?: string
debrisFieldId?: string // 残骸场ID用于回收任务 debrisFieldId?: string // 残骸场ID用于回收任务
missionType: MissionType missionType: MissionType
@@ -413,6 +424,8 @@ export interface MissionReport {
message: string // 任务结果描述 message: string // 任务结果描述
// 任务特定的详细信息 // 任务特定的详细信息
details?: { details?: {
// 通用:失败原因
failReason?: string
// 运输任务:运输的资源 // 运输任务:运输的资源
transportedResources?: Resources transportedResources?: Resources
// 殖民任务:新星球信息 // 殖民任务:新星球信息
@@ -423,6 +436,9 @@ export interface MissionReport {
remainingDebris?: Pick<Resources, 'metal' | 'crystal'> remainingDebris?: Pick<Resources, 'metal' | 'crystal'>
// 毁灭任务:摧毁的星球 // 毁灭任务:摧毁的星球
destroyedPlanetName?: string destroyedPlanetName?: string
// 毁灭任务:概率和死星损失
destructionChance?: number
deathstarsLost?: boolean
// 部署任务:部署的舰队 // 部署任务:部署的舰队
deployedFleet?: Partial<Fleet> deployedFleet?: Partial<Fleet>
// 导弹攻击任务:导弹信息 // 导弹攻击任务:导弹信息
@@ -436,6 +452,8 @@ export interface MissionReport {
foundFleet?: Partial<Fleet> foundFleet?: Partial<Fleet>
// 探险任务:损失的舰船 // 探险任务:损失的舰船
fleetLost?: Partial<Fleet> fleetLost?: Partial<Fleet>
// 侦查任务报告ID
spyReportId?: string
} }
read?: boolean read?: boolean
} }
@@ -501,6 +519,8 @@ export interface Planet {
maxFleetStorage: number // 舰队仓储上限 maxFleetStorage: number // 舰队仓储上限
isMoon: boolean // 是否为月球 isMoon: boolean // 是否为月球
parentPlanetId?: string // 如果是月球,指向母星的ID parentPlanetId?: string // 如果是月球,指向母星的ID
diameter?: number // 月球直径(km),用于销毁概率计算
jumpGateLastUsed?: number // 跳跃门上次使用时间戳(ms),用于冷却计算
} }
// 月球特殊配置 // 月球特殊配置
@@ -586,6 +606,11 @@ export interface Player {
hintsEnabled?: boolean // 是否启用弱引导提示默认true hintsEnabled?: boolean // 是否启用弱引导提示默认true
// 显示设置 // 显示设置
backgroundEnabled?: boolean // 是否启用背景动画默认false backgroundEnabled?: boolean // 是否启用背景动画默认false
// 舰队预设
fleetPresets?: FleetPreset[] // 舰队预设列表最多3个
// 成就系统
achievementStats?: AchievementStats // 成就统计数据
achievements?: Record<string, AchievementProgress> // 成就进度
} }
export interface NotificationSettings { export interface NotificationSettings {
@@ -624,7 +649,10 @@ export interface NPC {
note?: string // 玩家添加的备注 note?: string // 玩家添加的备注
planets: Planet[] planets: Planet[]
technologies: Record<TechnologyType, number> technologies: Record<TechnologyType, number>
difficulty: 'easy' | 'medium' | 'hard' difficulty: 'easy' | 'medium' | 'hard' // 保留兼容,不再使用
// 距离难度系统
difficultyLevel?: number // 基于距离的难度等级1-无限)
distanceToHomeworld?: number // 到玩家母星的距离
// 行为跟踪字段 // 行为跟踪字段
lastSpyTime?: number // 上次侦查玩家的时间 lastSpyTime?: number // 上次侦查玩家的时间
lastAttackTime?: number // 上次攻击玩家的时间 lastAttackTime?: number // 上次攻击玩家的时间
@@ -676,3 +704,121 @@ export interface TutorialProgress {
currentStep: string | null // 当前步骤ID currentStep: string | null // 当前步骤ID
skippedAt?: number // 跳过的时间戳 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 buildingValidation from '@/logic/buildingValidation'
import * as publicLogic from '@/logic/publicLogic' import * as publicLogic from '@/logic/publicLogic'
import * as officerLogic from '@/logic/officerLogic' import * as officerLogic from '@/logic/officerLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore() const gameStore = useGameStore()
const detailDialog = useDetailDialogStore() const detailDialog = useDetailDialogStore()
@@ -227,8 +228,9 @@
return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => { return (Object.values(BuildingType) as BuildingType[]).filter(buildingType => {
const config = BUILDINGS.value[buildingType] const config = BUILDINGS.value[buildingType]
if (planet.value!.isMoon) { if (planet.value!.isMoon) {
// 月球只能建造月球专属建筑 // 月球可以建造月球专属建筑 + 非行星专属建筑(如机器人工厂、船坞、机库等)
return config.moonOnly === true // OGame规则月球不能建造 planetOnly 的建筑(矿场、研究实验室、纳米工厂等)
return config.planetOnly !== true
} else { } else {
// 行星不能建造月球专属建筑 // 行星不能建造月球专属建筑
return config.moonOnly !== true return config.moonOnly !== true
@@ -245,6 +247,12 @@
gameStore.player.officers gameStore.player.officers
) )
if (!validation.valid) return { success: false, reason: validation.reason } 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) const queueItem = buildingValidation.executeBuildingUpgrade(gameStore.currentPlanet, buildingType, gameStore.player.officers)
gameStore.currentPlanet.buildQueue.push(queueItem) gameStore.currentPlanet.buildQueue.push(queueItem)
return { success: true } return { success: true }

View File

@@ -180,6 +180,7 @@
import * as publicLogic from '@/logic/publicLogic' import * as publicLogic from '@/logic/publicLogic'
import * as shipValidation from '@/logic/shipValidation' import * as shipValidation from '@/logic/shipValidation'
import * as shipLogic from '@/logic/shipLogic' import * as shipLogic from '@/logic/shipLogic'
import * as gameLogic from '@/logic/gameLogic'
const gameStore = useGameStore() const gameStore = useGameStore()
const detailDialog = useDetailDialogStore() const detailDialog = useDetailDialogStore()
@@ -236,6 +237,11 @@
if (!currentPlanet) return { success: false } if (!currentPlanet) return { success: false }
const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies) const validation = shipValidation.validateDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.technologies)
if (!validation.valid) return { success: false, reason: validation.reason } 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) const queueItem = shipValidation.executeDefenseBuild(currentPlanet, defenseType, quantity, gameStore.player.officers)
currentPlanet.buildQueue.push(queueItem) currentPlanet.buildQueue.push(queueItem)
return { success: true } return { success: true }

View File

@@ -7,12 +7,13 @@
<!-- 标签切换 --> <!-- 标签切换 -->
<Tabs v-model="activeTab" class="w-full"> <Tabs v-model="activeTab" class="w-full">
<TabsList class="grid w-full grid-cols-3"> <TabsList :class="['grid', 'w-full', showJumpGateTab ? 'grid-cols-4' : 'grid-cols-3']">
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value"> <TabsTrigger v-for="tab in visibleTabs" :key="tab.value" :value="tab.value">
{{ t(`fleetView.${tab.labelKey}`) }} {{ t(`fleetView.${tab.labelKey}`) }}
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1"> <Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
{{ gameStore.player.fleetMissions.length }} {{ gameStore.player.fleetMissions.length }}
</Badge> </Badge>
<Badge v-if="tab.value === 'jumpGate' && jumpGateReady" variant="default" class="ml-1"></Badge>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -59,6 +60,80 @@
</CardContent> </CardContent>
</Card> </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> <Card>
<CardHeader> <CardHeader>
@@ -93,13 +168,27 @@
<CardHeader> <CardHeader>
<CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle> <CardTitle>{{ t('fleetView.targetCoordinates') }}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent class="space-y-4">
<div class="grid grid-cols-3 gap-2 sm:gap-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"> <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> <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" /> <Input :id="coord.key" v-model.number="targetPosition[coord.key]" type="number" :min="1" :max="coord.max" placeholder="1" />
</div> </div>
</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> </CardContent>
</Card> </Card>
@@ -278,6 +367,108 @@
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </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> </Tabs>
<!-- 提示对话框 --> <!-- 提示对话框 -->
@@ -295,6 +486,40 @@
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </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> </div>
</template> </template>
@@ -307,7 +532,7 @@
import { computed, ref, onMounted, onUnmounted, watch } from 'vue' import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game' 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -329,7 +554,27 @@
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import UnlockRequirement from '@/components/UnlockRequirement.vue' import UnlockRequirement from '@/components/UnlockRequirement.vue'
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty' 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 { formatNumber, formatTime } from '@/utils/format'
import * as shipValidation from '@/logic/shipValidation' import * as shipValidation from '@/logic/shipValidation'
import * as fleetLogic from '@/logic/fleetLogic' import * as fleetLogic from '@/logic/fleetLogic'
@@ -337,6 +582,8 @@
import * as officerLogic from '@/logic/officerLogic' import * as officerLogic from '@/logic/officerLogic'
import * as publicLogic from '@/logic/publicLogic' import * as publicLogic from '@/logic/publicLogic'
import * as diplomaticLogic from '@/logic/diplomaticLogic' import * as diplomaticLogic from '@/logic/diplomaticLogic'
import * as gameLogic from '@/logic/gameLogic'
import * as moonLogic from '@/logic/moonLogic'
const route = useRoute() const route = useRoute()
const gameStore = useGameStore() const gameStore = useGameStore()
@@ -363,15 +610,127 @@
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel) return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
}) })
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet') const activeTab = ref<'fleet' | 'send' | 'missions' | 'jumpGate'>('fleet')
// Tab 配置 // Tab 配置
const fleetTabs = [ const fleetTabs = [
{ value: 'fleet', labelKey: 'fleetOverview' }, { value: 'fleet', labelKey: 'fleetOverview' },
{ value: 'send', labelKey: 'sendFleet' }, { value: 'send', labelKey: 'sendFleet' },
{ value: 'missions', labelKey: 'flightMissions' } { value: 'missions', labelKey: 'flightMissions' },
{ value: 'jumpGate', labelKey: 'jumpGate' }
] as const ] 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>>({ const selectedFleet = ref<Partial<Fleet>>({
[ShipType.LightFighter]: 0, [ShipType.LightFighter]: 0,
@@ -390,6 +749,20 @@
// 目标坐标 // 目标坐标
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 }) 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 }[] = [ const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
{ key: 'galaxy', max: 9 }, { key: 'galaxy', max: 9 },
@@ -463,6 +836,227 @@
// 是否为赠送模式 // 是否为赠送模式
const isGiftMode = ref(false) 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时自动禁用赠送模式 // 监听目标NPC变化当目标不再是NPC时自动禁用赠送模式
watch(targetNpc, newValue => { watch(targetNpc, newValue => {
if (!newValue && isGiftMode.value) { if (!newValue && isGiftMode.value) {
@@ -543,8 +1137,16 @@
if (!hasShips) return { valid: false, errorKey: 'fleetView.noShipsSelected' } 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 = const isSamePlanet =
targetPosition.value.galaxy === planet.value.position.galaxy && targetPosition.value.galaxy === planet.value.position.galaxy &&
targetPosition.value.system === planet.value.position.system && targetPosition.value.system === planet.value.position.system &&
@@ -591,7 +1193,8 @@
targetPosition: { galaxy: number; system: number; position: number }, targetPosition: { galaxy: number; system: number; position: number },
missionType: MissionType, missionType: MissionType,
fleet: Partial<Fleet>, 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 => { ): boolean => {
if (!gameStore.currentPlanet) return false if (!gameStore.currentPlanet) return false
const currentMissions = gameStore.player.fleetMissions.length const currentMissions = gameStore.player.fleetMissions.length
@@ -604,6 +1207,13 @@
gameStore.player.technologies gameStore.player.technologies
) )
if (!validation.valid) return false 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 const shouldDeductCargo = missionType === MissionType.Transport
shipValidation.executeFleetDispatch(gameStore.currentPlanet, fleet, validation.fuelNeeded!, shouldDeductCargo, cargo) shipValidation.executeFleetDispatch(gameStore.currentPlanet, fleet, validation.fuelNeeded!, shouldDeductCargo, cargo)
const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition) const distance = fleetLogic.calculateDistance(gameStore.currentPlanet.position, targetPosition)
@@ -620,6 +1230,11 @@
flightTime flightTime
) )
// 如果目标是月球,设置标记
if (isMoonTarget) {
mission.targetIsMoon = true
}
// 如果是赠送模式,标记任务 // 如果是赠送模式,标记任务
if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) { if (missionType === MissionType.Transport && isGiftMode.value && targetNpc.value) {
mission.isGift = true mission.isGift = true
@@ -655,7 +1270,8 @@
targetPosition.value, targetPosition.value,
selectedMission.value, selectedMission.value,
fleet, fleet,
selectedMission.value === MissionType.Transport ? cargo.value : undefined selectedMission.value === MissionType.Transport ? cargo.value : undefined,
targetIsMoon.value
) )
if (success) { if (success) {

View File

@@ -229,6 +229,15 @@
</p> </p>
</PopoverContent> </PopoverContent>
</Popover> </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)"> <Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Badge <Badge
@@ -260,6 +269,16 @@
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </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>
</div> </div>
<!-- 空位置 --> <!-- 空位置 -->
@@ -345,6 +364,16 @@
<p>{{ t('galaxyView.sendGift') }}</p> <p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </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"> <Tooltip v-if="!slot.planet">
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0"> <Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -417,6 +446,15 @@
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </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)"> <Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
<PopoverTrigger as-child> <PopoverTrigger as-child>
@@ -450,6 +488,16 @@
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </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>
<!-- PC端坐标 --> <!-- PC端坐标 -->
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
@@ -539,6 +587,16 @@
<p>{{ t('galaxyView.sendGift') }}</p> <p>{{ t('galaxyView.sendGift') }}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </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"> <Tooltip v-if="!slot.planet">
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0"> <Button @click="showPlanetActions(null, 'colonize', slot.position)" variant="outline" size="sm" class="h-8 w-8 p-0">
@@ -650,6 +708,97 @@
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </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> </div>
</template> </template>
@@ -681,10 +830,13 @@
AlertDialogTitle AlertDialogTitle
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import ResourceIcon from '@/components/ResourceIcon.vue' 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 { useRouter, useRoute } from 'vue-router'
import * as gameLogic from '@/logic/gameLogic' 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 gameStore = useGameStore()
const universeStore = useUniverseStore() const universeStore = useUniverseStore()
@@ -704,6 +856,12 @@
const missileTargetPlanet = ref<Planet | null>(null) const missileTargetPlanet = ref<Planet | null>(null)
const missileCount = ref(1) 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 selectedGalaxy = ref(1)
const selectedSystem = ref(1) const selectedSystem = ref(1)
const currentGalaxy = ref(1) const currentGalaxy = ref(1)
@@ -718,7 +876,7 @@
return npcStore.npcs.find(n => n.id === highlightNpcId.value) || null 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(() => { 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) const positions = gameLogic.generateSystemPositions(galaxy, system)
return positions.map(pos => { return positions.map(pos => {
const key = gameLogic.generatePositionKey(galaxy, system, pos.position) const key = gameLogic.generatePositionKey(galaxy, system, pos.position)
// 先从玩家星球中查找,再从宇宙地图中查找 // 先从玩家星球中查找(非月球),再从宇宙地图中查找
const planet = const planet =
gameStore.player.planets.find( 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] || universeStore.planets[key] ||
null 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 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) => { const switchToPlanet = (planetId: string) => {
gameStore.currentPlanetId = planetId gameStore.currentPlanetId = planetId
@@ -1030,4 +1222,172 @@
const secs = seconds % 60 const secs = seconds % 60
return `${minutes}:${secs.toString().padStart(2, '0')}` 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> </script>

View File

@@ -176,6 +176,7 @@
import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format' import { formatNumber, formatTime, formatDate, getResourceCostColor } from '@/utils/format'
import * as officerLogic from '@/logic/officerLogic' import * as officerLogic from '@/logic/officerLogic'
import * as resourceLogic from '@/logic/resourceLogic' import * as resourceLogic from '@/logic/resourceLogic'
import * as gameLogic from '@/logic/gameLogic'
import { useI18n } from '@/composables/useI18n' import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig' import { useGameConfig } from '@/composables/useGameConfig'
@@ -250,6 +251,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) { if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false return false
} }
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost) resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
gameStore.player.officers[officerType] = officerLogic.createActiveOfficer(officerType, duration) gameStore.player.officers[officerType] = officerLogic.createActiveOfficer(officerType, duration)
return true return true
@@ -276,6 +279,8 @@
if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) { if (!resourceLogic.checkResourcesAvailable(gameStore.currentPlanet.resources, cost)) {
return false return false
} }
// 追踪资源消耗(在扣除前)
gameLogic.trackResourceConsumption(gameStore.player, cost)
resourceLogic.deductResources(gameStore.currentPlanet.resources, cost) resourceLogic.deductResources(gameStore.currentPlanet.resources, cost)
const now = Date.now() const now = Date.now()
gameStore.player.officers[officerType] = officerLogic.renewOfficerExpiration(gameStore.player.officers[officerType], duration, 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"> <div v-if="!planet.isMoon && moon" class="mt-2">
<Button @click="switchToMoon" variant="outline" size="sm"> <Button @click="switchToMoon" variant="outline" size="sm">
<span class="mr-2">🌙</span>
{{ t('planet.switchToMoon') }} {{ t('planet.switchToMoon') }}
</Button> </Button>
</div> </div>

View File

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

View File

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