mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
Compare commits
36 Commits
revert-7-m
...
v1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20fb2bb6a4 | ||
|
|
752cade67c | ||
|
|
a689ce21b7 | ||
|
|
37045b432b | ||
|
|
a0ab4beaf4 | ||
|
|
53d5216e88 | ||
|
|
2ed15c4782 | ||
|
|
0da82802b8 | ||
|
|
d2465b5d4b | ||
|
|
e8590d54c7 | ||
|
|
2e3ac1231f | ||
|
|
99e4dbbb0d | ||
|
|
07ece4412f | ||
|
|
bde0532dbd | ||
|
|
d69b842c80 | ||
|
|
57fdc1b637 | ||
|
|
d700216cfc | ||
|
|
6813456d12 | ||
|
|
97db1324b6 | ||
|
|
ebd7eb1405 | ||
|
|
310372b8e2 | ||
|
|
d5a6dd49a1 | ||
|
|
f30676df07 | ||
|
|
690e6cbbf5 | ||
|
|
bd24ca02ae | ||
|
|
d9a8accad7 | ||
|
|
b166babf12 | ||
|
|
4aa4d9d350 | ||
|
|
60fd4135ec | ||
|
|
0bb9244214 | ||
|
|
cfcde0b024 | ||
|
|
053bd24855 | ||
|
|
7d1f36046d | ||
|
|
22ae07de90 | ||
|
|
a76909a2c7 | ||
|
|
8144f305e2 |
31
.github/ISSUE_TEMPLATE/BUG反馈.md
vendored
31
.github/ISSUE_TEMPLATE/BUG反馈.md
vendored
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: BUG反馈
|
|
||||||
about: 报告项目中发现的缺陷或问题
|
|
||||||
title: '[BUG] 简要描述问题'
|
|
||||||
labels: 'bug'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**问题描述**
|
|
||||||
清晰准确地描述遇到的问题
|
|
||||||
|
|
||||||
**重现步骤**
|
|
||||||
|
|
||||||
1. 第一步操作
|
|
||||||
2. 第二步操作
|
|
||||||
3. 出现问题的操作
|
|
||||||
|
|
||||||
**期望行为**
|
|
||||||
描述您认为正确的行为应该是怎样的
|
|
||||||
|
|
||||||
**实际行为**
|
|
||||||
描述实际发生的错误行为
|
|
||||||
|
|
||||||
**环境信息**
|
|
||||||
|
|
||||||
- 操作系统:
|
|
||||||
- 浏览器(如适用):
|
|
||||||
- 项目版本:
|
|
||||||
|
|
||||||
**截图或日志(可选)**
|
|
||||||
如果有错误截图或日志,请提供
|
|
||||||
19
.github/ISSUE_TEMPLATE/功能请求.md
vendored
19
.github/ISSUE_TEMPLATE/功能请求.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: 功能请求
|
|
||||||
about: 请求添加新功能或改进现有功能
|
|
||||||
title: '[功能] 简要描述功能'
|
|
||||||
labels: 'enhancement'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**功能描述**
|
|
||||||
清晰描述您希望添加的功能
|
|
||||||
|
|
||||||
**功能背景**
|
|
||||||
说明为什么需要这个功能,它能解决什么问题
|
|
||||||
|
|
||||||
**建议实现方案(可选)**
|
|
||||||
如果有具体的实现想法,可以在这里描述
|
|
||||||
|
|
||||||
**附加信息**
|
|
||||||
任何其他有助于理解这个功能的信息
|
|
||||||
19
.github/ISSUE_TEMPLATE/反馈建议.md
vendored
19
.github/ISSUE_TEMPLATE/反馈建议.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: 反馈建议
|
|
||||||
about: 为这个项目提出功能建议或改进意见
|
|
||||||
title: '[建议] 简要描述您的建议'
|
|
||||||
labels: 'enhancement'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的建议是什么?**
|
|
||||||
请清晰描述您希望添加的功能或改进点
|
|
||||||
|
|
||||||
**为什么需要这个功能/改进?**
|
|
||||||
说明这个建议会解决什么问题或带来什么价值
|
|
||||||
|
|
||||||
**您期望的实现方式(可选)**
|
|
||||||
如果有具体的实现想法,可以在这里描述
|
|
||||||
|
|
||||||
**附加信息(可选)**
|
|
||||||
任何其他有助于理解这个建议的信息
|
|
||||||
19
.github/ISSUE_TEMPLATE/文档改进.md
vendored
19
.github/ISSUE_TEMPLATE/文档改进.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: 文档改进
|
|
||||||
about: 报告文档问题或建议改进
|
|
||||||
title: '[文档] 简要描述问题'
|
|
||||||
labels: 'documentation'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档位置**
|
|
||||||
指出需要改进的文档路径或 URL
|
|
||||||
|
|
||||||
**当前问题**
|
|
||||||
描述当前文档存在的问题或不清晰的地方
|
|
||||||
|
|
||||||
**改进建议**
|
|
||||||
提出具体的改进建议
|
|
||||||
|
|
||||||
**附加信息(可选)**
|
|
||||||
任何其他有助于改进文档的信息
|
|
||||||
2
.github/workflows/github-pages.yml
vendored
2
.github/workflows/github-pages.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy Vue Project
|
name: 构建 Github Pages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
2
.github/workflows/ogame-vue-ts.yml
vendored
2
.github/workflows/ogame-vue-ts.yml
vendored
@@ -57,4 +57,4 @@ jobs:
|
|||||||
${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, github.sha) || '' }}
|
${{ vars.DOCKERHUB_USERNAME != '' && format('docker.io/{0}/ogame-vue-ts:{1}', vars.DOCKERHUB_USERNAME, github.sha) || '' }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue
|
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=OGame Vue Ts
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
FROM node:latest AS builder
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
RUN mkdir -p /workspace
|
RUN mkdir -p /workspace
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||||
|
RUN apk update && apk add git
|
||||||
RUN npm config set registry https://registry.npmmirror.com
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
RUN git clone https://github.com/setube/ogame-vue-ts.git
|
RUN git clone https://github.com/setube/ogame-vue-ts.git
|
||||||
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
|
RUN mv ./ogame-vue-ts/* . ; rm -rf ./ogame-vue-ts/
|
||||||
|
|
||||||
RUN npm install -g pnpm ; pnpm install;
|
RUN npm install -g pnpm ; pnpm install;
|
||||||
RUN pnpm build
|
RUN pnpm run build
|
||||||
|
|
||||||
# --- 第二阶段:Nginx ---
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@@ -213,13 +213,6 @@ The application supports full theme customization through Tailwind CSS variables
|
|||||||
|
|
||||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||||
|
|
||||||
### Issue Templates
|
|
||||||
We provide the following issue templates in both Chinese and English:
|
|
||||||
- Bug Report
|
|
||||||
- Feature Request
|
|
||||||
- Documentation Improvement
|
|
||||||
- eedback & Suggestion
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).
|
This work is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||||
|
|||||||
@@ -213,13 +213,6 @@ ogame-vue-ts/
|
|||||||
|
|
||||||
欢迎贡献!请随时提交 issue 或 pull request。
|
欢迎贡献!请随时提交 issue 或 pull request。
|
||||||
|
|
||||||
### Issue 模板
|
|
||||||
我们提供以下中英文 issue 模板:
|
|
||||||
- BUG反馈 / Bug Report
|
|
||||||
- 功能请求 / Feature Request
|
|
||||||
- 文档改进 / Documentation Improvement
|
|
||||||
- 反馈建议 / Feedback & Suggestion
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
本作品采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc/4.0/) 进行许可。
|
本作品采用 [知识共享署名-非商业性使用 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc/4.0/) 进行许可。
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -8,12 +8,8 @@
|
|||||||
"email": "1962257451@qq.com"
|
"email": "1962257451@qq.com"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.5",
|
"version": "1.4.0",
|
||||||
<<<<<<< Updated upstream
|
"buildDate": "2025/12/19 12:29:46",
|
||||||
"buildDate": "2025/12/15 21:21:23",
|
|
||||||
=======
|
|
||||||
"buildDate": "2025/12/15 21:59:38",
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -27,18 +23,16 @@
|
|||||||
"@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",
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"finalhandler": "^2.1.1",
|
"finalhandler": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.556.0",
|
"lucide-vue-next": "^0.556.0",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
|
"motion-v": "^1.7.4",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"reka-ui": "^2.6.1",
|
"reka-ui": "^2.6.1",
|
||||||
"serve-static": "^2.2.0",
|
"serve-static": "^2.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "4",
|
"vue-router": "4",
|
||||||
@@ -50,10 +44,13 @@
|
|||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"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",
|
||||||
|
"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",
|
||||||
"vite": "npm:rolldown-vite@7.2.5",
|
"vite": "npm:rolldown-vite@7.2.5",
|
||||||
|
|||||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -20,12 +20,6 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
class-variance-authority:
|
|
||||||
specifier: ^0.7.1
|
|
||||||
version: 0.7.1
|
|
||||||
clsx:
|
|
||||||
specifier: ^2.1.1
|
|
||||||
version: 2.1.1
|
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@@ -41,6 +35,9 @@ importers:
|
|||||||
marked:
|
marked:
|
||||||
specifier: ^17.0.1
|
specifier: ^17.0.1
|
||||||
version: 17.0.1
|
version: 17.0.1
|
||||||
|
motion-v:
|
||||||
|
specifier: ^1.7.4
|
||||||
|
version: 1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||||
@@ -53,9 +50,6 @@ importers:
|
|||||||
serve-static:
|
serve-static:
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
tailwind-merge:
|
|
||||||
specifier: ^3.4.0
|
|
||||||
version: 3.4.0
|
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
@@ -84,6 +78,12 @@ importers:
|
|||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.8.1
|
specifier: ^0.8.1
|
||||||
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
|
||||||
|
class-variance-authority:
|
||||||
|
specifier: ^0.7.1
|
||||||
|
version: 0.7.1
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
@@ -96,6 +96,9 @@ 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))
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.4.0
|
||||||
|
version: 3.4.0
|
||||||
tw-animate-css:
|
tw-animate-css:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
@@ -2103,6 +2106,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fresh@2.0.0:
|
fresh@2.0.0:
|
||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2244,6 +2261,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hey-listen@1.0.8:
|
||||||
|
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@@ -2772,6 +2792,18 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||||
|
|
||||||
|
motion-utils@12.23.6:
|
||||||
|
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||||
|
|
||||||
|
motion-v@1.7.4:
|
||||||
|
resolution: {integrity: sha512-YNDUAsany04wfI7YtHxQK3kxzNvh+OdFUk9GpA3+hMt7j6P+5WrVAAgr8kmPPoVza9EsJiAVhqoN3YYFN0Twrw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vueuse/core': '>=10.0.0'
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -6027,6 +6059,12 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
framer-motion@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
fs-extra@10.1.0:
|
fs-extra@10.1.0:
|
||||||
@@ -6208,6 +6246,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
hey-listen@1.0.8: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
hosted-git-info@4.1.0:
|
hosted-git-info@4.1.0:
|
||||||
@@ -6694,6 +6734,24 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
|
motion-dom@12.23.12:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.23.6
|
||||||
|
|
||||||
|
motion-utils@12.23.6: {}
|
||||||
|
|
||||||
|
motion-v@1.7.4(@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
|
framer-motion: 12.23.12
|
||||||
|
hey-listen: 1.0.8
|
||||||
|
motion-dom: 12.23.12
|
||||||
|
vue: 3.5.25(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@emotion/is-prop-valid'
|
||||||
|
- react
|
||||||
|
- react-dom
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|||||||
1058
src/App.vue
1058
src/App.vue
@@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<SidebarProvider :open="sidebarOpen" @update:open="sidebarOpen = $event">
|
<!-- 首页:无侧边栏/头部 -->
|
||||||
|
<template v-if="isHomePage">
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 其他页面:完整布局(含侧边栏) -->
|
||||||
|
<SidebarProvider v-else :open="sidebarOpen" @update:open="handleSidebarOpenChange">
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<!-- Logo -->
|
<!-- 标志 -->
|
||||||
<SidebarHeader class="border-b">
|
<SidebarHeader class="border-b">
|
||||||
<div class="flex items-center justify-center p-4 group-data-[collapsible=icon]:p-2">
|
<div class="flex items-center justify-center p-4 group-data-[collapsible=icon]:p-2">
|
||||||
<img src="@/assets/logo.svg" class="w-10 group-data-[collapsible=icon]:w-8" />
|
<img src="@/assets/logo.svg" class="w-10 group-data-[collapsible=icon]:w-8" />
|
||||||
@@ -17,6 +23,7 @@
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
|
data-tutorial="planet-selector"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
|
class="w-full justify-between h-auto px-3 py-2.5 border-2 hover:bg-accent hover:border-primary transition-colors"
|
||||||
>
|
>
|
||||||
@@ -46,12 +53,11 @@
|
|||||||
{{ t('planet.switchPlanet') }}
|
{{ t('planet.switchPlanet') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-0.5 max-h-80 overflow-y-auto">
|
<div class="space-y-0.5 max-h-80 overflow-y-auto">
|
||||||
|
<div v-for="p in gameStore.player.planets" :key="p.id" class="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-for="p in gameStore.player.planets"
|
|
||||||
:key="p.id"
|
|
||||||
@click="switchToPlanet(p.id)"
|
@click="switchToPlanet(p.id)"
|
||||||
:variant="p.id === planet.id ? 'secondary' : 'ghost'"
|
:variant="p.id === planet.id ? 'secondary' : 'ghost'"
|
||||||
class="w-full justify-start h-auto py-2 px-2"
|
class="flex-1 justify-start h-auto py-2 px-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2 w-full min-w-0">
|
<div class="flex items-start gap-2 w-full min-w-0">
|
||||||
@@ -59,6 +65,15 @@
|
|||||||
<div class="flex-1 min-w-0 text-left">
|
<div class="flex-1 min-w-0 text-left">
|
||||||
<div class="flex items-center gap-1.5 mb-0.5">
|
<div class="flex items-center gap-1.5 mb-0.5">
|
||||||
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
<span class="truncate font-medium text-sm">{{ p.name }}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-2 w-2 p-0 flex-shrink-0"
|
||||||
|
@click.stop="openRenameDialog(p.id, p.name)"
|
||||||
|
:title="t('planet.renamePlanet')"
|
||||||
|
>
|
||||||
|
<Pencil class="h-2 w-2" />
|
||||||
|
</Button>
|
||||||
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
|
<Badge v-if="p.isMoon" variant="outline" class="text-[10px] px-1 py-0 h-4">
|
||||||
{{ t('planet.moon') }}
|
{{ t('planet.moon') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -71,6 +86,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
@@ -94,11 +110,15 @@
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
<!-- 导航菜单 -->
|
<!-- 导航菜单 -->
|
||||||
<SidebarGroup>
|
<SidebarGroup data-tutorial="navigation">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in navItems" :key="item.path">
|
<SidebarMenuItem v-for="item in navItems" :key="item.path">
|
||||||
<SidebarMenuButton as-child :is-active="$route.path === item.path" :tooltip="item.name.value">
|
<SidebarMenuButton
|
||||||
<RouterLink :to="item.path">
|
:data-nav-path="item.path"
|
||||||
|
:is-active="$route.path === item.path"
|
||||||
|
:tooltip="item.name.value"
|
||||||
|
@click="handleNavClick(item.path, $event)"
|
||||||
|
>
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
<span>{{ item.name.value }}</span>
|
<span>{{ item.name.value }}</span>
|
||||||
<!-- 未读消息数量 -->
|
<!-- 未读消息数量 -->
|
||||||
@@ -109,13 +129,16 @@
|
|||||||
{{ unreadMessagesCount }}
|
{{ unreadMessagesCount }}
|
||||||
</SidebarMenuBadge>
|
</SidebarMenuBadge>
|
||||||
<!-- 正在执行的舰队任务数量 -->
|
<!-- 正在执行的舰队任务数量 -->
|
||||||
<SidebarMenuBadge
|
<SidebarMenuBadge v-if="item.path === '/fleet' && activeFleetMissionsCount > 0" class="bg-primary text-primary-foreground">
|
||||||
v-if="item.path === '/fleet' && activeFleetMissionsCount > 0"
|
|
||||||
class="bg-primary text-primary-foreground"
|
|
||||||
>
|
|
||||||
{{ activeFleetMissionsCount }}
|
{{ activeFleetMissionsCount }}
|
||||||
</SidebarMenuBadge>
|
</SidebarMenuBadge>
|
||||||
</RouterLink>
|
<!-- 未读外交报告数量 -->
|
||||||
|
<SidebarMenuBadge
|
||||||
|
v-if="item.path === '/diplomacy' && unreadDiplomaticReportsCount > 0"
|
||||||
|
class="bg-destructive text-destructive-foreground"
|
||||||
|
>
|
||||||
|
{{ unreadDiplomaticReportsCount }}
|
||||||
|
</SidebarMenuBadge>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -177,7 +200,7 @@
|
|||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<div class="flex flex-col h-full overflow-hidden pt-[60px]">
|
<div class="flex flex-col h-full pt-[60px]">
|
||||||
<!-- 顶部资源栏 - 固定定位 -->
|
<!-- 顶部资源栏 - 固定定位 -->
|
||||||
<header
|
<header
|
||||||
v-if="planet"
|
v-if="planet"
|
||||||
@@ -189,12 +212,21 @@
|
|||||||
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
|
<div class="grid items-center gap-3 sm:gap-6" style="grid-template-columns: auto 1fr auto">
|
||||||
<!-- 左侧:汉堡菜单(移动端)/ 占位(PC端) -->
|
<!-- 左侧:汉堡菜单(移动端)/ 占位(PC端) -->
|
||||||
<div>
|
<div>
|
||||||
<SidebarTrigger class="lg:hidden" />
|
<SidebarTrigger class="lg:hidden" data-tutorial="mobile-menu" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 资源显示 - PC端居中,移动端可折叠 -->
|
<!-- 资源显示 - PC端居中,移动端可折叠 -->
|
||||||
<div :class="['flex items-center gap-3 sm:gap-6 justify-center', resourceBarExpanded ? 'hidden' : 'overflow-x-auto']">
|
<!-- 关键:min-w-0 + overflow-hidden,避免横向滚动内容溢出覆盖左侧菜单按钮 -->
|
||||||
<div v-for="resourceType in resourceTypes" :key="resourceType.key" class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
|
<div class="min-w-0 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="resource-bar flex items-center gap-3 sm:gap-6 justify-start sm:justify-center"
|
||||||
|
:class="resourceBarExpanded ? 'hidden' : 'overflow-x-auto'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="resourceType in resourceTypes"
|
||||||
|
:key="resourceType.key"
|
||||||
|
class="flex items-center gap-1.5 sm:gap-2 flex-shrink-0"
|
||||||
|
>
|
||||||
<ResourceIcon :type="resourceType.key" size="md" />
|
<ResourceIcon :type="resourceType.key" size="md" />
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<!-- 电力显示净产量和效率 -->
|
<!-- 电力显示净产量和效率 -->
|
||||||
@@ -215,7 +247,8 @@
|
|||||||
class="text-xs sm:text-sm font-medium truncate"
|
class="text-xs sm:text-sm font-medium truncate"
|
||||||
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
:class="getResourceColor(planet.resources[resourceType.key], capacity?.[resourceType.key] || Infinity)"
|
||||||
>
|
>
|
||||||
{{ formatNumber(planet.resources[resourceType.key]) }} / {{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
{{ formatNumber(planet.resources[resourceType.key]) }} /
|
||||||
|
{{ formatNumber(capacity?.[resourceType.key] || 0) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
|
<p class="text-[10px] sm:text-xs text-muted-foreground truncate">
|
||||||
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
|
+{{ formatNumber(Math.round((production?.[resourceType.key] || 0) / 60)) }}/{{ t('resources.perMinute') }}
|
||||||
@@ -224,24 +257,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
<ChevronDown v-if="!resourceBarExpanded" class="h-4 w-4" />
|
||||||
<ChevronUp v-else class="h-4 w-4" />
|
<ChevronUp v-else class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- 建造队列状态 -->
|
|
||||||
<div v-if="planet.buildQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
||||||
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.building') }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="gameStore.player.researchQueue.length > 0" class="flex items-center gap-1.5 text-xs sm:text-sm">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />
|
|
||||||
<span class="text-muted-foreground hidden sm:inline">{{ t('queue.researching') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,85 +328,54 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<!-- 即将到来的敌对舰队警告 -->
|
<!-- 即将到来的敌对舰队警告 -->
|
||||||
<IncomingFleetAlerts
|
<IncomingFleetAlerts @open-panel="openEnemyAlertPanel" />
|
||||||
v-if="gameStore.player.incomingFleetAlerts && gameStore.player.incomingFleetAlerts.length > 0"
|
|
||||||
:alerts="gameStore.player.incomingFleetAlerts"
|
|
||||||
@mark-as-read="removeIncomingFleetAlert"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 建造队列 -->
|
|
||||||
<div
|
|
||||||
v-if="planet && (planet.buildQueue.length > 0 || gameStore.player.researchQueue.length > 0)"
|
|
||||||
class="bg-card border-b px-4 sm:px-6 py-4.5"
|
|
||||||
>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<!-- 建造队列 -->
|
|
||||||
<div v-for="item in planet.buildQueue" :key="item.id" class="space-y-1.5">
|
|
||||||
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
|
||||||
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-green-500 animate-pulse flex-shrink-0" />
|
|
||||||
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
|
||||||
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
|
|
||||||
<template v-if="item.type === 'ship' || item.type === 'defense'">
|
|
||||||
→ {{ t('queue.quantity') }} {{ item.quantity }}
|
|
||||||
</template>
|
|
||||||
<template v-else>→ {{ t('queue.level') }} {{ item.targetLevel }}</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
|
||||||
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
|
|
||||||
{{ formatTime(getRemainingTime(item)) }}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
@click="handleCancelBuild(item.id)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
|
||||||
>
|
|
||||||
{{ t('queue.cancel') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
|
|
||||||
</div>
|
|
||||||
<!-- 研究队列 -->
|
|
||||||
<div v-for="item in gameStore.player.researchQueue" :key="item.id" class="space-y-1.5">
|
|
||||||
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
|
||||||
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
|
||||||
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />
|
|
||||||
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
|
||||||
<span class="text-muted-foreground hidden sm:inline flex-shrink-0 text-[10px] sm:text-xs">
|
|
||||||
→ {{ t('queue.level') }} {{ item.targetLevel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
|
||||||
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
|
|
||||||
{{ formatTime(getRemainingTime(item)) }}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
@click="handleCancelResearch(item.id)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
|
||||||
>
|
|
||||||
{{ t('queue.cancel') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<main class="flex-1 overflow-y-auto">
|
<main class="flex-1">
|
||||||
<div class="animate-fade-in">
|
<Transition name="page" mode="out-in">
|
||||||
|
<div :key="$route.fullPath" class="h-full">
|
||||||
|
<!-- 背景动画开启时 -->
|
||||||
|
<template v-if="gameStore.player.backgroundEnabled">
|
||||||
|
<StarsBackground v-if="isDark" :factor="0.05" :speed="50" star-color="#fff" class="h-full">
|
||||||
|
<div class="relative z-10 h-full">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
</StarsBackground>
|
||||||
|
|
||||||
|
<div v-else class="relative h-full w-full overflow-hidden">
|
||||||
|
<div class="relative z-10 h-full">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ParticlesBg class="absolute inset-0 z-0" :quantity="100" :ease="100" color="#000" :staticity="10" refresh />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 背景动画关闭时 -->
|
||||||
|
<div v-else class="h-full">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|
||||||
|
<!-- 右下角固定通知按钮 -->
|
||||||
|
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||||
|
<!-- 返回顶部 -->
|
||||||
|
<BackToTop />
|
||||||
|
|
||||||
|
<!-- 队列通知 -->
|
||||||
|
<QueueNotifications />
|
||||||
|
|
||||||
|
<!-- 外交通知 -->
|
||||||
|
<DiplomaticNotifications />
|
||||||
|
|
||||||
|
<!-- 敌方警报 -->
|
||||||
|
<EnemyAlertNotifications ref="enemyAlertNotificationsRef" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 确认对话框 -->
|
<!-- 确认对话框 -->
|
||||||
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
<AlertDialog :open="confirmDialogOpen" @update:open="confirmDialogOpen = $event">
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
@@ -405,25 +398,54 @@
|
|||||||
<!-- 更新弹窗 -->
|
<!-- 更新弹窗 -->
|
||||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||||
|
|
||||||
|
<!-- 弱引导提示系统 -->
|
||||||
|
<HintToast />
|
||||||
|
|
||||||
<!-- Toast 通知 -->
|
<!-- Toast 通知 -->
|
||||||
<Sonner position="top-center" />
|
<Sonner position="top-center" />
|
||||||
|
|
||||||
|
<!-- 重命名星球对话框 -->
|
||||||
|
<Dialog v-model:open="renameDialogOpen">
|
||||||
|
<DialogContent class="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ t('planet.renamePlanetTitle') }}</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">{{ t('planet.renamePlanetTitle') }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="py-4">
|
||||||
|
<Input v-model="newPlanetName" :placeholder="t('planet.planetNamePlaceholder')" @keyup.enter="confirmRenamePlanet" />
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="renameDialogOpen = false">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="confirmRenamePlanet" :disabled="!newPlanetName.trim()">
|
||||||
|
{{ t('planet.rename') }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, computed, ref, watch } from 'vue'
|
||||||
import { RouterView, RouterLink } from 'vue-router'
|
import { RouterView, useRouter } from 'vue-router'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useUniverseStore } from '@/stores/universeStore'
|
import { useUniverseStore } from '@/stores/universeStore'
|
||||||
import { useNPCStore } from '@/stores/npcStore'
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
|
import { localeNames, detectBrowserLocale, type Locale } from '@/locales'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Progress } from '@/components/ui/progress'
|
|
||||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
|
import IncomingFleetAlerts from '@/components/IncomingFleetAlerts.vue'
|
||||||
|
import DiplomaticNotifications from '@/components/DiplomaticNotifications.vue'
|
||||||
|
import EnemyAlertNotifications from '@/components/EnemyAlertNotifications.vue'
|
||||||
|
import QueueNotifications from '@/components/QueueNotifications.vue'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -451,11 +473,15 @@
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import DetailDialog from '@/components/DetailDialog.vue'
|
import DetailDialog from '@/components/DetailDialog.vue'
|
||||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||||
|
import HintToast from '@/components/HintToast.vue'
|
||||||
|
import BackToTop from '@/components/BackToTop.vue'
|
||||||
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
import Sonner from '@/components/ui/sonner/Sonner.vue'
|
||||||
import { MissionType } from '@/types/game'
|
import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game'
|
||||||
import type { BuildQueueItem, FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
|
import type { FleetMission, NPC, MissileAttack } from '@/types/game'
|
||||||
|
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||||
import type { VersionInfo } from '@/utils/versionCheck'
|
import type { VersionInfo } from '@/utils/versionCheck'
|
||||||
import { formatNumber, formatTime, getResourceColor } from '@/utils/format'
|
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||||
|
import { scaleNumber, scaleResources } from '@/utils/speed'
|
||||||
import {
|
import {
|
||||||
Moon,
|
Moon,
|
||||||
Sun,
|
Sun,
|
||||||
@@ -476,7 +502,8 @@
|
|||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Handshake
|
Handshake,
|
||||||
|
Pencil
|
||||||
} 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'
|
||||||
@@ -489,30 +516,202 @@
|
|||||||
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
|
import * as npcGrowthLogic from '@/logic/npcGrowthLogic'
|
||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||||
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { migrateGameData } from '@/utils/migration'
|
import { migrateGameData } from '@/utils/migration'
|
||||||
import { checkLatestVersion } from '@/utils/versionCheck'
|
import { checkLatestVersion } from '@/utils/versionCheck'
|
||||||
|
import { StarsBackground } from '@/components/ui/bg-stars'
|
||||||
|
import { ParticlesBg } from '@/components/ui/particles-bg'
|
||||||
|
|
||||||
// 执行数据迁移(在 store 初始化之前)
|
// 执行数据迁移(在 store 初始化之前)
|
||||||
migrateGameData()
|
migrateGameData()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const universeStore = useUniverseStore()
|
const universeStore = useUniverseStore()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||||
|
const enemyAlertNotificationsRef = ref<InstanceType<typeof EnemyAlertNotifications> | null>(null)
|
||||||
// ConfirmDialog 状态
|
// ConfirmDialog 状态
|
||||||
const confirmDialogOpen = ref(false)
|
const confirmDialogOpen = ref(false)
|
||||||
const confirmDialogTitle = ref('')
|
const confirmDialogTitle = ref('')
|
||||||
const confirmDialogMessage = ref('')
|
const confirmDialogMessage = ref('')
|
||||||
const innerWidth = computed(() => window.innerWidth)
|
const innerWidth = computed(() => window.innerWidth)
|
||||||
const confirmDialogAction = ref<(() => void) | null>(null)
|
const confirmDialogAction = ref<(() => void) | null>(null)
|
||||||
|
|
||||||
// 更新弹窗状态
|
// 更新弹窗状态
|
||||||
const showUpdateDialog = ref(false)
|
const showUpdateDialog = ref(false)
|
||||||
const updateInfo = ref<VersionInfo | null>(null)
|
const updateInfo = ref<VersionInfo | null>(null)
|
||||||
|
// 所有可用的语言选项
|
||||||
|
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||||
|
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
|
||||||
|
// PC端(≥1024px)默认打开,移动端默认关闭
|
||||||
|
const sidebarOpen = ref(window.innerWidth >= 1024)
|
||||||
|
// 移动端资源栏展开状态
|
||||||
|
const resourceBarExpanded = ref(false)
|
||||||
|
const npcUpdateCounter = ref(0) // 累计秒数
|
||||||
|
const NPC_UPDATE_INTERVAL = 5 // 每1秒更新一次NPC,确保发育速度与玩家相当
|
||||||
|
// NPC行为系统更新函数(侦查和攻击决策)
|
||||||
|
const npcBehaviorCounter = ref(0)
|
||||||
|
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
|
||||||
|
|
||||||
|
// 游戏循环定时器
|
||||||
|
const gameLoop = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
const pointsUpdateInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
const konamiCleanup = ref<(() => void) | null>(null)
|
||||||
|
const versionCheckInterval = ref<ReturnType<typeof setInterval> | null>(null) // 重命名星球相关状态
|
||||||
|
const renameDialogOpen = ref(false)
|
||||||
|
const renamingPlanetId = ref<string | null>(null)
|
||||||
|
const newPlanetName = ref('')
|
||||||
|
// 功能解锁要求配置
|
||||||
|
const featureRequirements: Record<string, { building: BuildingType; level: number }> = {
|
||||||
|
'/research': { building: BuildingType.ResearchLab, level: 1 },
|
||||||
|
'/shipyard': { building: BuildingType.Shipyard, level: 1 },
|
||||||
|
'/defense': { building: BuildingType.Shipyard, level: 1 },
|
||||||
|
'/fleet': { building: BuildingType.Shipyard, level: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为首页
|
||||||
|
const isHomePage = computed(() => router.currentRoute.value.path === '/')
|
||||||
|
|
||||||
|
// 定义 planet computed(需要在 watch 之前定义)
|
||||||
|
const planet = computed(() => gameStore.currentPlanet)
|
||||||
|
|
||||||
|
// 资源类型配置
|
||||||
|
const resourceTypes = [
|
||||||
|
{ key: 'metal' as const },
|
||||||
|
{ key: 'crystal' as const },
|
||||||
|
{ key: 'deuterium' as const },
|
||||||
|
{ key: 'energy' as const },
|
||||||
|
{ key: 'darkMatter' as const }
|
||||||
|
]
|
||||||
|
|
||||||
|
const navItems = computed(() => [
|
||||||
|
{ name: computed(() => t('nav.overview')), path: '/overview', icon: Home },
|
||||||
|
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
|
||||||
|
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
|
||||||
|
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
|
||||||
|
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
|
||||||
|
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
|
||||||
|
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
|
||||||
|
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
||||||
|
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
||||||
|
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
||||||
|
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
||||||
|
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
||||||
|
// GM菜单在启用GM模式时显示
|
||||||
|
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
|
||||||
|
])
|
||||||
|
|
||||||
|
// 使用直接计算,不再缓存
|
||||||
|
const production = computed(() => {
|
||||||
|
if (!planet.value) return null
|
||||||
|
const now = Date.now()
|
||||||
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||||
|
const base = resourceLogic.calculateResourceProduction(planet.value, {
|
||||||
|
resourceProductionBonus: bonuses.resourceProductionBonus,
|
||||||
|
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
|
||||||
|
energyProductionBonus: bonuses.energyProductionBonus
|
||||||
|
})
|
||||||
|
return scaleResources(base, gameStore.gameSpeed)
|
||||||
|
})
|
||||||
|
|
||||||
|
const capacity = computed(() => {
|
||||||
|
if (!planet.value) return null
|
||||||
|
const now = Date.now()
|
||||||
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||||
|
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 电力消耗
|
||||||
|
const energyConsumption = computed(() => {
|
||||||
|
if (!planet.value) return 0
|
||||||
|
return scaleNumber(resourceLogic.calculateEnergyConsumption(planet.value), gameStore.gameSpeed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 净电力(产量 - 消耗)
|
||||||
|
const netEnergy = computed(() => {
|
||||||
|
if (!planet.value || !production.value) return 0
|
||||||
|
return production.value.energy - energyConsumption.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 未读消息数量
|
||||||
|
const unreadMessagesCount = computed(() => {
|
||||||
|
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
|
||||||
|
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
|
||||||
|
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
|
||||||
|
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
|
||||||
|
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
|
||||||
|
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
|
||||||
|
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
|
||||||
|
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
|
||||||
|
})
|
||||||
|
|
||||||
|
// 正在执行的舰队任务数量(包括飞行中的导弹)
|
||||||
|
const activeFleetMissionsCount = computed(() => {
|
||||||
|
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
|
||||||
|
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
|
||||||
|
return fleetMissions + flyingMissiles
|
||||||
|
})
|
||||||
|
|
||||||
|
// 未读外交报告数量
|
||||||
|
const unreadDiplomaticReportsCount = computed(() => {
|
||||||
|
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 月球相关
|
||||||
|
const moon = computed(() => {
|
||||||
|
if (!planet.value || planet.value.isMoon) return null
|
||||||
|
return gameStore.getMoonForPlanet(planet.value.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasMoon = computed(() => !!moon.value)
|
||||||
|
|
||||||
|
const handleNotification = (type: string, itemType: string, level?: number) => {
|
||||||
|
const settings = gameStore.notificationSettings
|
||||||
|
if (!settings) return
|
||||||
|
|
||||||
|
// 检查主开关
|
||||||
|
if (!settings.browser && !settings.inApp) return
|
||||||
|
|
||||||
|
// 检查具体类型开关
|
||||||
|
let typeKey: 'construction' | 'research'
|
||||||
|
let title = ''
|
||||||
|
let body = ''
|
||||||
|
|
||||||
|
if (type === 'building') {
|
||||||
|
typeKey = 'construction'
|
||||||
|
const buildingType = itemType as BuildingType
|
||||||
|
const name = BUILDINGS.value[buildingType]?.name || itemType
|
||||||
|
title = t('notifications.constructionComplete')
|
||||||
|
body = `${name} Lv ${level}`
|
||||||
|
} else if (type === 'technology') {
|
||||||
|
typeKey = 'research'
|
||||||
|
const technologyType = itemType as TechnologyType
|
||||||
|
const name = TECHNOLOGIES.value[technologyType]?.name || itemType
|
||||||
|
title = t('notifications.researchComplete')
|
||||||
|
body = `${name} Lv ${level}`
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.types[typeKey]) return
|
||||||
|
|
||||||
|
// browser
|
||||||
|
if (settings.browser && 'Notification' in window && Notification.permission === 'granted') {
|
||||||
|
const shouldSuppress = settings.suppressInFocus && document.hasFocus()
|
||||||
|
if (!shouldSuppress) {
|
||||||
|
new Notification(title, { body, icon: '/favicon.ico' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toast
|
||||||
|
if (settings.inApp) {
|
||||||
|
toast.success(title, { description: body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleConfirmDialogConfirm = () => {
|
const handleConfirmDialogConfirm = () => {
|
||||||
if (confirmDialogAction.value) {
|
if (confirmDialogAction.value) {
|
||||||
@@ -521,31 +720,25 @@
|
|||||||
confirmDialogOpen.value = false
|
confirmDialogOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有可用的语言选项
|
|
||||||
const locales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
|
||||||
|
|
||||||
// 侧边栏状态(不持久化,根据屏幕尺寸初始化)
|
|
||||||
// PC端(≥1024px)默认打开,移动端默认关闭
|
|
||||||
const sidebarOpen = ref(window.innerWidth >= 1024)
|
|
||||||
|
|
||||||
// 移动端资源栏展开状态
|
|
||||||
const resourceBarExpanded = ref(false)
|
|
||||||
|
|
||||||
const initGame = async () => {
|
const initGame = async () => {
|
||||||
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
const shouldInit = gameLogic.shouldInitializeGame(gameStore.player.planets)
|
||||||
if (!shouldInit) {
|
if (!shouldInit) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
// 计算离线收益(直接同步计算)
|
// 计算离线收益(直接同步计算,应用游戏速度)
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
||||||
gameStore.player.planets.forEach(planet => {
|
gameStore.player.planets.forEach(planet => {
|
||||||
resourceLogic.updatePlanetResources(planet, now, bonuses)
|
resourceLogic.updatePlanetResources(planet, now, bonuses, gameStore.gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 只在没有NPC星球时才生成(首次加载已有玩家数据时)
|
// 只在没有NPC星球时才生成(首次加载已有玩家数据时)
|
||||||
if (Object.keys(universeStore.planets).length === 0) {
|
if (Object.keys(universeStore.planets).length === 0) {
|
||||||
generateNPCPlanets()
|
generateNPCPlanets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化或更新玩家积分
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
|
gameStore.player = gameLogic.initializePlayer(gameStore.player.id, t('common.playerName'))
|
||||||
@@ -554,6 +747,8 @@
|
|||||||
gameStore.currentPlanetId = initialPlanet.id
|
gameStore.currentPlanetId = initialPlanet.id
|
||||||
// 新玩家初始化时生成NPC星球
|
// 新玩家初始化时生成NPC星球
|
||||||
generateNPCPlanets()
|
generateNPCPlanets()
|
||||||
|
// 初始化玩家积分
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateNPCPlanets = () => {
|
const generateNPCPlanets = () => {
|
||||||
@@ -568,13 +763,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateGame = async () => {
|
const updateGame = async () => {
|
||||||
if (gameStore.isPaused) return
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
if (gameStore.isPaused) return
|
||||||
gameStore.gameTime = now
|
gameStore.gameTime = now
|
||||||
// 检查军官过期
|
// 检查军官过期
|
||||||
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
||||||
// 处理游戏更新(建造队列、研究队列等)
|
// 处理游戏更新(建造队列、研究队列等)
|
||||||
const result = gameLogic.processGameUpdate(gameStore.player, now)
|
const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification)
|
||||||
gameStore.player.researchQueue = result.updatedResearchQueue
|
gameStore.player.researchQueue = result.updatedResearchQueue
|
||||||
// 处理舰队任务
|
// 处理舰队任务
|
||||||
gameStore.player.fleetMissions.forEach(mission => {
|
gameStore.player.fleetMissions.forEach(mission => {
|
||||||
@@ -613,6 +808,27 @@
|
|||||||
|
|
||||||
// NPC行为系统更新(侦查和攻击决策)
|
// NPC行为系统更新(侦查和攻击决策)
|
||||||
updateNPCBehavior(1)
|
updateNPCBehavior(1)
|
||||||
|
|
||||||
|
// 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
||||||
|
const eliminatedNpcIds = diplomaticLogic.checkAndHandleEliminatedNPCs(npcStore.npcs, gameStore.player, gameStore.locale)
|
||||||
|
if (eliminatedNpcIds.length > 0) {
|
||||||
|
// 从universeStore中移除被消灭NPC的星球数据
|
||||||
|
eliminatedNpcIds.forEach(npcId => {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||||
|
if (npc && npc.planets) {
|
||||||
|
// 遍历NPC的所有星球,从universeStore中删除
|
||||||
|
npc.planets.forEach(planet => {
|
||||||
|
const planetKey = gameLogic.generatePositionKey(planet.position.galaxy, planet.position.system, planet.position.position)
|
||||||
|
if (universeStore.planets[planetKey]) {
|
||||||
|
delete universeStore.planets[planetKey]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从NPC列表中移除被消灭的NPC
|
||||||
|
npcStore.npcs = npcStore.npcs.filter(npc => !eliminatedNpcIds.includes(npc.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processMissionArrival = async (mission: FleetMission) => {
|
const processMissionArrival = async (mission: FleetMission) => {
|
||||||
@@ -636,6 +852,8 @@
|
|||||||
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
|
const originPlanetName = originPlanet?.name || t('fleetView.unknownPlanet')
|
||||||
|
|
||||||
if (mission.missionType === MissionType.Transport) {
|
if (mission.missionType === MissionType.Transport) {
|
||||||
|
// 在处理任务之前保存货物信息(因为processTransportArrival会清空cargo)
|
||||||
|
const transportedResources = { ...mission.cargo }
|
||||||
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
const result = fleetLogic.processTransportArrival(mission, targetPlanet, gameStore.player, npcStore.npcs)
|
||||||
// 生成运输任务报告
|
// 生成运输任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
@@ -654,7 +872,7 @@
|
|||||||
success: result.success,
|
success: result.success,
|
||||||
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
|
message: result.success ? t('missionReports.transportSuccess') : t('missionReports.transportFailed'),
|
||||||
details: {
|
details: {
|
||||||
transportedResources: mission.cargo
|
transportedResources
|
||||||
},
|
},
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
@@ -711,7 +929,7 @@
|
|||||||
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
|
const spyReport = fleetLogic.processSpyArrival(mission, targetPlanet, gameStore.player, null, npcStore.npcs)
|
||||||
if (spyReport) gameStore.player.spyReports.push(spyReport)
|
if (spyReport) gameStore.player.spyReports.push(spyReport)
|
||||||
} else if (mission.missionType === MissionType.Deploy) {
|
} else if (mission.missionType === MissionType.Deploy) {
|
||||||
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id)
|
const deployed = fleetLogic.processDeployArrival(mission, targetPlanet, gameStore.player.id, gameStore.player.technologies)
|
||||||
// 生成部署任务报告
|
// 生成部署任务报告
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
gameStore.player.missionReports = []
|
gameStore.player.missionReports = []
|
||||||
@@ -726,14 +944,14 @@
|
|||||||
targetPlanetId: targetPlanet?.id,
|
targetPlanetId: targetPlanet?.id,
|
||||||
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: deployed.success,
|
||||||
message: deployed ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
|
message: deployed.success ? t('missionReports.deploySuccess') : t('missionReports.deployFailed'),
|
||||||
details: {
|
details: {
|
||||||
deployedFleet: mission.fleet
|
deployedFleet: mission.fleet
|
||||||
},
|
},
|
||||||
read: false
|
read: false
|
||||||
})
|
})
|
||||||
if (deployed) {
|
if (deployed.success && !deployed.overflow) {
|
||||||
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
|
const missionIndex = gameStore.player.fleetMissions.indexOf(mission)
|
||||||
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
|
if (missionIndex > -1) gameStore.player.fleetMissions.splice(missionIndex, 1)
|
||||||
return
|
return
|
||||||
@@ -812,6 +1030,15 @@
|
|||||||
|
|
||||||
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
||||||
// 星球被摧毁
|
// 星球被摧毁
|
||||||
|
|
||||||
|
// 处理外交关系(如果目标是NPC星球)
|
||||||
|
if (targetPlanet && targetPlanet.ownerId) {
|
||||||
|
const planetOwner = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
|
||||||
|
if (planetOwner) {
|
||||||
|
diplomaticLogic.handlePlanetDestructionReputation(gameStore.player, targetPlanet, planetOwner, npcStore.npcs, gameStore.locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 从玩家星球列表中移除(如果是玩家的星球)
|
// 从玩家星球列表中移除(如果是玩家的星球)
|
||||||
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
|
const planetIndex = gameStore.player.planets.findIndex(p => p.id === destroyResult.planetId)
|
||||||
if (planetIndex > -1) {
|
if (planetIndex > -1) {
|
||||||
@@ -821,6 +1048,60 @@
|
|||||||
delete universeStore.planets[targetKey]
|
delete universeStore.planets[targetKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (mission.missionType === MissionType.Expedition) {
|
||||||
|
// 处理远征任务
|
||||||
|
const expeditionResult = fleetLogic.processExpeditionArrival(mission)
|
||||||
|
|
||||||
|
// 生成远征任务报告
|
||||||
|
if (!gameStore.player.missionReports) {
|
||||||
|
gameStore.player.missionReports = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据事件类型生成不同的报告消息
|
||||||
|
let reportMessage = ''
|
||||||
|
let reportDetails: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
switch (expeditionResult.eventType) {
|
||||||
|
case 'resources':
|
||||||
|
reportMessage = t('missionReports.expeditionResources')
|
||||||
|
reportDetails = { foundResources: expeditionResult.resources }
|
||||||
|
break
|
||||||
|
case 'darkMatter':
|
||||||
|
reportMessage = t('missionReports.expeditionDarkMatter')
|
||||||
|
reportDetails = { foundResources: expeditionResult.resources }
|
||||||
|
break
|
||||||
|
case 'fleet':
|
||||||
|
reportMessage = t('missionReports.expeditionFleet')
|
||||||
|
reportDetails = { foundFleet: expeditionResult.fleet }
|
||||||
|
break
|
||||||
|
case 'pirates':
|
||||||
|
reportMessage = expeditionResult.fleetLost
|
||||||
|
? t('missionReports.expeditionPiratesAttack')
|
||||||
|
: t('missionReports.expeditionPiratesEscaped')
|
||||||
|
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
||||||
|
break
|
||||||
|
case 'aliens':
|
||||||
|
reportMessage = expeditionResult.fleetLost
|
||||||
|
? t('missionReports.expeditionAliensAttack')
|
||||||
|
: t('missionReports.expeditionAliensEscaped')
|
||||||
|
reportDetails = expeditionResult.fleetLost ? { fleetLost: expeditionResult.fleetLost } : {}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
reportMessage = t('missionReports.expeditionNothing')
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.player.missionReports.push({
|
||||||
|
id: `mission-report-${mission.id}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
missionType: MissionType.Expedition,
|
||||||
|
originPlanetId: mission.originPlanetId,
|
||||||
|
originPlanetName,
|
||||||
|
targetPosition: mission.targetPosition,
|
||||||
|
success: expeditionResult.eventType !== 'nothing',
|
||||||
|
message: reportMessage,
|
||||||
|
details: reportDetails,
|
||||||
|
read: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,9 +1203,22 @@
|
|||||||
|
|
||||||
// 如果生成残骸场,添加到宇宙残骸场列表
|
// 如果生成残骸场,添加到宇宙残骸场列表
|
||||||
if (attackResult.debrisField) {
|
if (attackResult.debrisField) {
|
||||||
|
const existingDebris = universeStore.debrisFields[attackResult.debrisField.id]
|
||||||
|
if (existingDebris) {
|
||||||
|
// 累加残骸资源
|
||||||
|
universeStore.debrisFields[attackResult.debrisField.id] = {
|
||||||
|
...existingDebris,
|
||||||
|
resources: {
|
||||||
|
metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal,
|
||||||
|
crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新残骸场
|
||||||
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
universeStore.debrisFields[attackResult.debrisField.id] = attackResult.debrisField
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 移除即将到来的警告(攻击已到达)
|
// 移除即将到来的警告(攻击已到达)
|
||||||
removeIncomingFleetAlertById(mission.id)
|
removeIncomingFleetAlertById(mission.id)
|
||||||
@@ -1010,6 +1304,28 @@
|
|||||||
// 应用损失到目标星球
|
// 应用损失到目标星球
|
||||||
missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses)
|
missileLogic.applyMissileAttackResult(targetPlanet, impactResult.defenseLosses)
|
||||||
|
|
||||||
|
// 如果目标是NPC的星球,扣除外交好感度
|
||||||
|
if (targetPlanet.ownerId && targetPlanet.ownerId !== gameStore.player.id) {
|
||||||
|
const targetNpc = npcStore.npcs.find(npc => npc.id === targetPlanet.ownerId)
|
||||||
|
if (targetNpc) {
|
||||||
|
// 导弹攻击扣除好感度
|
||||||
|
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||||
|
const reputationLoss = REPUTATION_CHANGES.ATTACK / 2 // 导弹攻击的好感度惩罚是普通攻击的一半
|
||||||
|
|
||||||
|
// 更新NPC对玩家的关系(统一使用 npc.relations 作为唯一数据源)
|
||||||
|
if (!targetNpc.relations) {
|
||||||
|
targetNpc.relations = {}
|
||||||
|
}
|
||||||
|
const npcRelation = diplomaticLogic.getOrCreateRelation(targetNpc.relations, targetNpc.id, gameStore.player.id)
|
||||||
|
targetNpc.relations[gameStore.player.id] = diplomaticLogic.updateReputation(
|
||||||
|
npcRelation,
|
||||||
|
reputationLoss,
|
||||||
|
DiplomaticEventType.Attack,
|
||||||
|
t('diplomacy.reports.wasAttackedByMissile')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 标记导弹攻击为已到达
|
// 标记导弹攻击为已到达
|
||||||
missileAttack.status = 'arrived'
|
missileAttack.status = 'arrived'
|
||||||
|
|
||||||
@@ -1043,13 +1359,9 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除即将到来的舰队警告
|
// 打开敌方警报面板
|
||||||
const removeIncomingFleetAlert = (alert: IncomingFleetAlert) => {
|
const openEnemyAlertPanel = () => {
|
||||||
if (!gameStore.player.incomingFleetAlerts) return
|
enemyAlertNotificationsRef.value?.open()
|
||||||
const index = gameStore.player.incomingFleetAlerts.indexOf(alert)
|
|
||||||
if (index > -1) {
|
|
||||||
gameStore.player.incomingFleetAlerts.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeIncomingFleetAlertById = (missionId: string) => {
|
const removeIncomingFleetAlertById = (missionId: string) => {
|
||||||
@@ -1060,16 +1372,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NPC成长系统更新函数
|
|
||||||
let npcUpdateCounter = 0 // 累计秒数
|
|
||||||
const NPC_UPDATE_INTERVAL = 10 // 每10秒更新一次NPC,减少性能开销
|
|
||||||
|
|
||||||
const updateNPCGrowth = (deltaSeconds: number) => {
|
const updateNPCGrowth = (deltaSeconds: number) => {
|
||||||
// 累积时间
|
// 累积时间
|
||||||
npcUpdateCounter += deltaSeconds
|
npcUpdateCounter.value += deltaSeconds
|
||||||
|
|
||||||
// 只在达到更新间隔时才执行
|
// 只在达到更新间隔时才执行
|
||||||
if (npcUpdateCounter < NPC_UPDATE_INTERVAL) {
|
if (npcUpdateCounter.value < NPC_UPDATE_INTERVAL) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,15 +1394,35 @@
|
|||||||
|
|
||||||
// 这是NPC的星球
|
// 这是NPC的星球
|
||||||
if (!npcMap.has(planet.ownerId)) {
|
if (!npcMap.has(planet.ownerId)) {
|
||||||
|
// 为每个NPC设置随机的初始冷却时间,避免所有NPC同时行动
|
||||||
|
const now = Date.now()
|
||||||
|
const randomSpyOffset = Math.random() * 240 * 1000 // 0-4分钟的随机延迟
|
||||||
|
const randomAttackOffset = Math.random() * 480 * 1000 // 0-8分钟的随机延迟
|
||||||
|
|
||||||
|
// 初始化NPC与玩家的中立关系
|
||||||
|
const initialRelations: Record<string, any> = {}
|
||||||
|
initialRelations[gameStore.player.id] = {
|
||||||
|
fromId: planet.ownerId,
|
||||||
|
toId: gameStore.player.id,
|
||||||
|
reputation: 0,
|
||||||
|
status: 'neutral' as const,
|
||||||
|
lastUpdated: now,
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
|
||||||
npcMap.set(planet.ownerId, {
|
npcMap.set(planet.ownerId, {
|
||||||
id: planet.ownerId,
|
id: planet.ownerId,
|
||||||
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
name: `NPC-${planet.ownerId.substring(0, 8)}`,
|
||||||
planets: [],
|
planets: [],
|
||||||
technologies: {}, // 初始化空科技树
|
technologies: {}, // 初始化空科技树
|
||||||
difficulty: 'medium' as const, // 默认中等难度
|
difficulty: 'medium' as const, // 默认中等难度
|
||||||
relations: {}, // 外交关系
|
relations: initialRelations, // 外交关系(默认与玩家中立)
|
||||||
allies: [], // 盟友列表
|
allies: [], // 盟友列表
|
||||||
enemies: [] // 敌人列表
|
enemies: [], // 敌人列表
|
||||||
|
lastSpyTime: now - randomSpyOffset, // 设置随机的上次侦查时间
|
||||||
|
lastAttackTime: now - randomAttackOffset, // 设置随机的上次攻击时间
|
||||||
|
fleetMissions: [], // 舰队任务
|
||||||
|
playerSpyReports: {} // 对玩家的侦查报告
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,9 +1451,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保所有NPC都有间谍探测器(修复旧版本保存的数据)
|
||||||
|
if (npcStore.npcs.length > 0) {
|
||||||
|
npcGrowthLogic.ensureNPCSpyProbes(npcStore.npcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保所有NPC都与玩家建立了关系(修复旧版本保存的数据)
|
||||||
|
if (npcStore.npcs.length > 0) {
|
||||||
|
const now = Date.now()
|
||||||
|
npcStore.npcs.forEach(npc => {
|
||||||
|
if (!npc.relations) {
|
||||||
|
npc.relations = {}
|
||||||
|
}
|
||||||
|
// 如果NPC没有与玩家的关系,建立中立关系
|
||||||
|
if (!npc.relations[gameStore.player.id]) {
|
||||||
|
npc.relations[gameStore.player.id] = {
|
||||||
|
fromId: npc.id,
|
||||||
|
toId: gameStore.player.id,
|
||||||
|
reputation: 0,
|
||||||
|
status: 'neutral' as const,
|
||||||
|
lastUpdated: now,
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有NPC,直接返回
|
// 如果没有NPC,直接返回
|
||||||
if (npcStore.npcs.length === 0) {
|
if (npcStore.npcs.length === 0) {
|
||||||
npcUpdateCounter = 0
|
npcUpdateCounter.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,130 +1490,127 @@
|
|||||||
npcs: npcStore.npcs
|
npcs: npcStore.npcs
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用累积的时间更新每个NPC
|
// 使用累积的时间更新每个NPC(应用游戏速度倍率)
|
||||||
npcStore.npcs.forEach(npc => {
|
npcStore.npcs.forEach(npc => {
|
||||||
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter)
|
npcGrowthLogic.updateNPCGrowth(npc, gameState, npcUpdateCounter.value, gameStore.gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重置计数器
|
// 重置计数器
|
||||||
npcUpdateCounter = 0
|
npcUpdateCounter.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// NPC行为系统更新函数(侦查和攻击决策)
|
|
||||||
let npcBehaviorCounter = 0
|
|
||||||
const NPC_BEHAVIOR_INTERVAL = 5 // 每5秒检查一次NPC行为
|
|
||||||
|
|
||||||
const updateNPCBehavior = (deltaSeconds: number) => {
|
const updateNPCBehavior = (deltaSeconds: number) => {
|
||||||
// 累积时间
|
// 累积时间
|
||||||
npcBehaviorCounter += deltaSeconds
|
npcBehaviorCounter.value += deltaSeconds
|
||||||
|
|
||||||
// 只在达到更新间隔时才执行
|
// 只在达到更新间隔时才执行
|
||||||
if (npcBehaviorCounter < NPC_BEHAVIOR_INTERVAL) {
|
if (npcBehaviorCounter.value < NPC_BEHAVIOR_INTERVAL) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有NPC,直接返回
|
// 如果没有NPC,直接返回
|
||||||
if (npcStore.npcs.length === 0) {
|
if (npcStore.npcs.length === 0) {
|
||||||
npcBehaviorCounter = 0
|
npcBehaviorCounter.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const allPlanets = Object.values(universeStore.planets)
|
// 合并玩家星球和NPC星球到allPlanets(NPC需要能够侦查和攻击玩家星球)
|
||||||
|
const allPlanets = [...gameStore.player.planets, ...Object.values(universeStore.planets)]
|
||||||
|
|
||||||
// 更新每个NPC的行为
|
// 计算当前所有正在进行的侦查和攻击任务数量
|
||||||
|
let activeSpyMissions = 0
|
||||||
|
let activeAttackMissions = 0
|
||||||
npcStore.npcs.forEach(npc => {
|
npcStore.npcs.forEach(npc => {
|
||||||
npcBehaviorLogic.updateNPCBehavior(npc, gameStore.player, allPlanets, universeStore.debrisFields, now)
|
if (npc.fleetMissions) {
|
||||||
|
npc.fleetMissions.forEach(mission => {
|
||||||
|
if (mission.status === 'outbound') {
|
||||||
|
if (mission.missionType === 'spy') {
|
||||||
|
activeSpyMissions++
|
||||||
|
} else if (mission.missionType === 'attack') {
|
||||||
|
activeAttackMissions++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
npcBehaviorCounter = 0
|
// 获取并发限制配置
|
||||||
}
|
const config = npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points)
|
||||||
|
|
||||||
// 游戏循环定时器
|
// 更新每个NPC的行为(随机顺序,避免总是优先处理同一批NPC)
|
||||||
let gameLoop: ReturnType<typeof setInterval> | null = null
|
const shuffledNpcs = [...npcStore.npcs].sort(() => Math.random() - 0.5)
|
||||||
let konamiCleanup: (() => void) | null = null
|
shuffledNpcs.forEach(npc => {
|
||||||
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
|
// 在更新前检查当前并发数,如果已达上限则跳过该NPC
|
||||||
|
npcBehaviorLogic.updateNPCBehaviorWithLimit(npc, gameStore.player, allPlanets, universeStore.debrisFields, now, {
|
||||||
|
activeSpyMissions,
|
||||||
|
activeAttackMissions,
|
||||||
|
config
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新计算当前并发数(因为可能新增了任务)
|
||||||
|
activeSpyMissions = 0
|
||||||
|
activeAttackMissions = 0
|
||||||
|
npcStore.npcs.forEach(n => {
|
||||||
|
if (n.fleetMissions) {
|
||||||
|
n.fleetMissions.forEach(mission => {
|
||||||
|
if (mission.status === 'outbound') {
|
||||||
|
if (mission.missionType === 'spy') activeSpyMissions++
|
||||||
|
else if (mission.missionType === 'attack') activeAttackMissions++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
npcBehaviorCounter.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 启动游戏循环
|
// 启动游戏循环
|
||||||
const startGameLoop = () => {
|
const startGameLoop = () => {
|
||||||
|
if (gameStore.isPaused) return
|
||||||
// 清理旧的定时器
|
// 清理旧的定时器
|
||||||
if (gameLoop) {
|
if (gameLoop.value) {
|
||||||
clearInterval(gameLoop)
|
clearInterval(gameLoop.value)
|
||||||
}
|
}
|
||||||
// 根据游戏速度计算间隔时间
|
// 游戏循环固定为1秒,避免高倍速时的卡顿
|
||||||
const interval = 1000 / (gameStore.gameSpeed || 1)
|
// gameSpeed 只作用于资源产出和时间消耗的倍率
|
||||||
|
const interval = 1000
|
||||||
// 启动新的游戏循环
|
// 启动新的游戏循环
|
||||||
gameLoop = setInterval(() => {
|
gameLoop.value = setInterval(() => {
|
||||||
updateGame()
|
updateGame()
|
||||||
}, interval)
|
}, interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听游戏速度变化,重新启动游戏循环
|
// 停止游戏循环
|
||||||
watch(
|
const stopGameLoop = () => {
|
||||||
() => gameStore.gameSpeed,
|
if (gameLoop.value) {
|
||||||
() => {
|
clearInterval(gameLoop.value)
|
||||||
if (gameLoop) {
|
gameLoop.value = null
|
||||||
startGameLoop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化游戏
|
// 启动积分更新定时器(每10秒更新一次)
|
||||||
onMounted(async () => {
|
const startPointsUpdate = () => {
|
||||||
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
|
if (pointsUpdateInterval.value) {
|
||||||
const isFirstVisit = gameStore.player.planets.length === 0
|
clearInterval(pointsUpdateInterval.value)
|
||||||
if (isFirstVisit) {
|
|
||||||
gameStore.locale = detectBrowserLocale()
|
|
||||||
}
|
}
|
||||||
await initGame()
|
pointsUpdateInterval.value = setInterval(() => {
|
||||||
// 启动游戏循环
|
if (!gameStore.isPaused) {
|
||||||
startGameLoop()
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
// 启动科乐美秘籍监听
|
|
||||||
konamiCleanup = setupKonamiCode()
|
|
||||||
// 首次检查版本(被动检测)
|
|
||||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
|
||||||
gameStore.player.lastVersionCheckTime = time
|
|
||||||
})
|
|
||||||
if (versionInfo) {
|
|
||||||
updateInfo.value = versionInfo
|
|
||||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
|
||||||
duration: Infinity,
|
|
||||||
dismissible: true,
|
|
||||||
action: {
|
|
||||||
label: t('settings.viewUpdate'),
|
|
||||||
onClick: () => {
|
|
||||||
showUpdateDialog.value = true
|
|
||||||
}
|
}
|
||||||
|
}, 10000) // 10秒更新一次
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
// 启动版本检查定时器(每5分钟被动检查一次)
|
|
||||||
versionCheckInterval = setInterval(async () => {
|
|
||||||
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
|
||||||
gameStore.player.lastVersionCheckTime = time
|
|
||||||
})
|
|
||||||
if (versionInfo) {
|
|
||||||
updateInfo.value = versionInfo
|
|
||||||
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
|
||||||
duration: Infinity,
|
|
||||||
dismissible: true,
|
|
||||||
action: {
|
|
||||||
label: t('settings.viewUpdate'),
|
|
||||||
onClick: () => {
|
|
||||||
showUpdateDialog.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 5 * 60 * 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 清理定时器
|
// 处理取消建造事件
|
||||||
onUnmounted(() => {
|
const handleCancelBuildEvent = (event: CustomEvent) => {
|
||||||
if (gameLoop) clearInterval(gameLoop)
|
handleCancelBuild(event.detail)
|
||||||
if (konamiCleanup) konamiCleanup()
|
}
|
||||||
if (versionCheckInterval) clearInterval(versionCheckInterval)
|
|
||||||
})
|
// 处理取消研究事件
|
||||||
|
const handleCancelResearchEvent = (event: CustomEvent) => {
|
||||||
|
handleCancelResearch(event.detail)
|
||||||
|
}
|
||||||
|
|
||||||
// 科乐美秘籍:上上下下左左右右BA
|
// 科乐美秘籍:上上下下左左右右BA
|
||||||
const setupKonamiCode = () => {
|
const setupKonamiCode = () => {
|
||||||
@@ -1292,91 +1643,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义 planet computed(需要在 watch 之前定义)
|
// 打开重命名对话框
|
||||||
const planet = computed(() => gameStore.currentPlanet)
|
const openRenameDialog = (planetId: string, currentName: string) => {
|
||||||
|
renamingPlanetId.value = planetId
|
||||||
|
newPlanetName.value = currentName
|
||||||
|
renameDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const navItems = computed(() => [
|
// 确认重命名
|
||||||
{ name: computed(() => t('nav.overview')), path: '/', icon: Home },
|
const confirmRenamePlanet = () => {
|
||||||
{ name: computed(() => t('nav.buildings')), path: '/buildings', icon: Building2 },
|
if (!renamingPlanetId.value || !newPlanetName.value.trim()) return
|
||||||
{ name: computed(() => t('nav.research')), path: '/research', icon: FlaskConical },
|
|
||||||
{ name: computed(() => t('nav.shipyard')), path: '/shipyard', icon: Ship },
|
|
||||||
{ name: computed(() => t('nav.defense')), path: '/defense', icon: Shield },
|
|
||||||
{ name: computed(() => t('nav.fleet')), path: '/fleet', icon: Rocket },
|
|
||||||
{ name: computed(() => t('nav.officers')), path: '/officers', icon: Users },
|
|
||||||
{ name: computed(() => t('nav.simulator')), path: '/battle-simulator', icon: Swords },
|
|
||||||
{ name: computed(() => t('nav.galaxy')), path: '/galaxy', icon: Globe },
|
|
||||||
{ name: computed(() => t('nav.diplomacy')), path: '/diplomacy', icon: Handshake },
|
|
||||||
{ name: computed(() => t('nav.messages')), path: '/messages', icon: Mail },
|
|
||||||
{ name: computed(() => t('nav.settings')), path: '/settings', icon: Settings },
|
|
||||||
// GM菜单在启用GM模式时显示
|
|
||||||
...(gameStore.player.isGMEnabled ? [{ name: computed(() => t('nav.gm')), path: '/gm', icon: Wrench }] : [])
|
|
||||||
])
|
|
||||||
|
|
||||||
// 使用直接计算,不再缓存
|
const targetPlanet = gameStore.player.planets.find(p => p.id === renamingPlanetId.value)
|
||||||
const production = computed(() => {
|
if (targetPlanet) {
|
||||||
if (!planet.value) return null
|
targetPlanet.name = newPlanetName.value.trim()
|
||||||
const now = Date.now()
|
}
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
|
||||||
return resourceLogic.calculateResourceProduction(planet.value, {
|
renameDialogOpen.value = false
|
||||||
resourceProductionBonus: bonuses.resourceProductionBonus,
|
renamingPlanetId.value = null
|
||||||
darkMatterProductionBonus: bonuses.darkMatterProductionBonus,
|
newPlanetName.value = ''
|
||||||
energyProductionBonus: bonuses.energyProductionBonus
|
}
|
||||||
})
|
|
||||||
|
// 检查功能是否解锁
|
||||||
|
const checkFeatureUnlocked = (path: string): { unlocked: boolean; requirement?: { building: BuildingType; level: number } } => {
|
||||||
|
const requirement = featureRequirements[path]
|
||||||
|
if (!requirement) {
|
||||||
|
return { unlocked: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLevel = planet.value?.buildings[requirement.building] || 0
|
||||||
|
return {
|
||||||
|
unlocked: currentLevel >= requirement.level,
|
||||||
|
requirement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理导航点击
|
||||||
|
const handleNavClick = (path: string, event: Event) => {
|
||||||
|
const { unlocked, requirement } = checkFeatureUnlocked(path)
|
||||||
|
|
||||||
|
if (!unlocked && requirement) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const buildingName = BUILDINGS.value[requirement.building]?.name || requirement.building
|
||||||
|
const currentLevel = planet.value?.buildings[requirement.building] || 0
|
||||||
|
|
||||||
|
toast.warning(t('common.featureLocked'), {
|
||||||
|
description: `${t('common.requiredBuilding')}: ${buildingName} Lv ${requirement.level} (${t(
|
||||||
|
'common.currentLevel'
|
||||||
|
)}: Lv ${currentLevel})`,
|
||||||
|
action: {
|
||||||
|
label: t('common.goToBuildings'),
|
||||||
|
onClick: () => router.push('/buildings')
|
||||||
|
},
|
||||||
|
duration: 3000
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const capacity = computed(() => {
|
// 功能已解锁,正常导航
|
||||||
if (!planet.value) return null
|
router.push(path)
|
||||||
const now = Date.now()
|
}
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, now)
|
|
||||||
return resourceLogic.calculateResourceCapacity(planet.value, bonuses.storageCapacityBonus)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 电力消耗
|
|
||||||
const energyConsumption = computed(() => {
|
|
||||||
if (!planet.value) return 0
|
|
||||||
return resourceLogic.calculateEnergyConsumption(planet.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 净电力(产量 - 消耗)
|
|
||||||
const netEnergy = computed(() => {
|
|
||||||
if (!planet.value || !production.value) return 0
|
|
||||||
return production.value.energy - energyConsumption.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 未读消息数量
|
|
||||||
const unreadMessagesCount = computed(() => {
|
|
||||||
const unreadBattles = gameStore.player.battleReports.filter(r => !r.read).length
|
|
||||||
const unreadSpies = gameStore.player.spyReports.filter(r => !r.read).length
|
|
||||||
const unreadSpied = gameStore.player.spiedNotifications?.filter(n => !n.read).length || 0
|
|
||||||
const unreadMissions = gameStore.player.missionReports?.filter(r => !r.read).length || 0
|
|
||||||
const unreadNPCActivity = gameStore.player.npcActivityNotifications?.filter(n => !n.read).length || 0
|
|
||||||
const unreadGifts = gameStore.player.giftNotifications?.filter(n => !n.read).length || 0
|
|
||||||
const unreadGiftRejected = gameStore.player.giftRejectedNotifications?.filter(n => !n.read).length || 0
|
|
||||||
return unreadBattles + unreadSpies + unreadSpied + unreadMissions + unreadNPCActivity + unreadGifts + unreadGiftRejected
|
|
||||||
})
|
|
||||||
|
|
||||||
// 正在执行的舰队任务数量(包括飞行中的导弹)
|
|
||||||
const activeFleetMissionsCount = computed(() => {
|
|
||||||
const fleetMissions = gameStore.player.fleetMissions.filter(m => m.status === 'outbound' || m.status === 'returning').length
|
|
||||||
const flyingMissiles = gameStore.player.missileAttacks?.filter(m => m.status === 'flying').length || 0
|
|
||||||
return fleetMissions + flyingMissiles
|
|
||||||
})
|
|
||||||
|
|
||||||
// 资源类型配置
|
|
||||||
const resourceTypes = [
|
|
||||||
{ key: 'metal' as const },
|
|
||||||
{ key: 'crystal' as const },
|
|
||||||
{ key: 'deuterium' as const },
|
|
||||||
{ key: 'energy' as const },
|
|
||||||
{ key: 'darkMatter' as const }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 月球相关
|
|
||||||
const moon = computed(() => {
|
|
||||||
if (!planet.value || planet.value.isMoon) return null
|
|
||||||
return gameStore.getMoonForPlanet(planet.value.id)
|
|
||||||
})
|
|
||||||
const hasMoon = computed(() => !!moon.value)
|
|
||||||
|
|
||||||
// 切换到月球
|
// 切换到月球
|
||||||
const switchToMoon = () => {
|
const switchToMoon = () => {
|
||||||
@@ -1402,33 +1730,9 @@
|
|||||||
sidebarOpen.value = !sidebarOpen.value
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取队列项的名称
|
// 处理侧边栏打开/关闭状态变化
|
||||||
const getItemName = (item: BuildQueueItem): string => {
|
const handleSidebarOpenChange = (open: boolean) => {
|
||||||
if (item.type === 'building' || item.type === 'demolish') {
|
sidebarOpen.value = open
|
||||||
const buildingName = t(`buildings.${item.itemType}`)
|
|
||||||
return item.type === 'demolish' ? `${t('buildingsView.demolish')} - ${buildingName}` : buildingName
|
|
||||||
} else if (item.type === 'technology') {
|
|
||||||
return t(`technologies.${item.itemType}`)
|
|
||||||
} else if (item.type === 'ship') {
|
|
||||||
return t(`ships.${item.itemType}`)
|
|
||||||
} else if (item.type === 'defense') {
|
|
||||||
return t(`defenses.${item.itemType}`)
|
|
||||||
}
|
|
||||||
return item.itemType
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取剩余时间
|
|
||||||
const getRemainingTime = (item: BuildQueueItem): number => {
|
|
||||||
const now = Date.now()
|
|
||||||
return Math.max(0, Math.floor((item.endTime - now) / 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取队列进度
|
|
||||||
const getQueueProgress = (item: BuildQueueItem): number => {
|
|
||||||
const now = Date.now()
|
|
||||||
const total = item.endTime - item.startTime
|
|
||||||
const elapsed = now - item.startTime
|
|
||||||
return Math.min(100, Math.max(0, (elapsed / total) * 100))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消建造
|
// 取消建造
|
||||||
@@ -1466,6 +1770,92 @@
|
|||||||
}
|
}
|
||||||
confirmDialogOpen.value = true
|
confirmDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听暂停状态变化
|
||||||
|
watch(
|
||||||
|
() => gameStore.isPaused,
|
||||||
|
isPaused => {
|
||||||
|
if (isPaused) {
|
||||||
|
stopGameLoop()
|
||||||
|
} else {
|
||||||
|
startGameLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化游戏
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 如果是首次访问(没有星球数据),使用浏览器语言自动检测
|
||||||
|
const isFirstVisit = gameStore.player.planets.length === 0
|
||||||
|
if (isFirstVisit) {
|
||||||
|
gameStore.locale = detectBrowserLocale()
|
||||||
|
}
|
||||||
|
await initGame()
|
||||||
|
// 启动游戏循环
|
||||||
|
startGameLoop()
|
||||||
|
// 启动积分更新定时器
|
||||||
|
startPointsUpdate()
|
||||||
|
// 启动科乐美秘籍监听
|
||||||
|
konamiCleanup.value = setupKonamiCode()
|
||||||
|
|
||||||
|
// 添加队列取消事件监听
|
||||||
|
window.addEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||||
|
window.addEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||||
|
|
||||||
|
// 首次检查版本(被动检测)
|
||||||
|
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||||
|
gameStore.player.lastVersionCheckTime = time
|
||||||
|
})
|
||||||
|
if (versionInfo) {
|
||||||
|
updateInfo.value = versionInfo
|
||||||
|
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||||
|
duration: Infinity,
|
||||||
|
dismissible: true,
|
||||||
|
action: {
|
||||||
|
label: t('settings.viewUpdate'),
|
||||||
|
onClick: () => {
|
||||||
|
showUpdateDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 启动版本检查定时器(每5分钟被动检查一次)
|
||||||
|
versionCheckInterval.value = setInterval(async () => {
|
||||||
|
const versionInfo = await checkLatestVersion(gameStore.player.lastVersionCheckTime || 0, (time: number) => {
|
||||||
|
gameStore.player.lastVersionCheckTime = time
|
||||||
|
})
|
||||||
|
if (versionInfo) {
|
||||||
|
updateInfo.value = versionInfo
|
||||||
|
toast.info(t('settings.newVersionAvailable', { version: versionInfo.version }), {
|
||||||
|
duration: Infinity,
|
||||||
|
dismissible: true,
|
||||||
|
action: {
|
||||||
|
label: t('settings.viewUpdate'),
|
||||||
|
onClick: () => {
|
||||||
|
showUpdateDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during game initialization:', error)
|
||||||
|
// 即使初始化失败,也尝试启动基本的游戏循环
|
||||||
|
startGameLoop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (gameLoop.value) clearInterval(gameLoop.value)
|
||||||
|
if (pointsUpdateInterval.value) clearInterval(pointsUpdateInterval.value)
|
||||||
|
if (konamiCleanup.value) konamiCleanup.value()
|
||||||
|
if (versionCheckInterval.value) clearInterval(versionCheckInterval.value)
|
||||||
|
// 移除队列取消事件监听
|
||||||
|
window.removeEventListener('cancel-build', handleCancelBuildEvent as EventListener)
|
||||||
|
window.removeEventListener('cancel-research', handleCancelResearchEvent as EventListener)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
95
src/assets/main.css
Normal file
95
src/assets/main.css
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.21 0.006 285.885);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.141 0.005 285.823);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.141 0.005 285.823);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.274 0.006 286.033);
|
||||||
|
--input: oklch(0.274 0.006 286.033);
|
||||||
|
--ring: oklch(0.442 0.017 285.786);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
48
src/components/BackToTop.vue
Normal file
48
src/components/BackToTop.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="translate-y-4 opacity-0"
|
||||||
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
|
leave-to-class="translate-y-4 opacity-0"
|
||||||
|
>
|
||||||
|
<Button v-if="isVisible" variant="outline" size="icon" @click="scrollToTop">
|
||||||
|
<ChevronUp class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ChevronUp } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
// 显示阈值(滚动超过这个距离才显示按钮)
|
||||||
|
const SCROLL_THRESHOLD = 300
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
|
||||||
|
// 监听滚动事件
|
||||||
|
const handleScroll = () => {
|
||||||
|
isVisible.value = window.scrollY > SCROLL_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
// 丝滑返回顶部
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
// 初始检查
|
||||||
|
handleScroll()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -40,15 +40,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 胜利者 -->
|
<!-- 胜利者 -->
|
||||||
<div class="text-center p-4 rounded-lg" :class="getWinnerStyle(report.winner)">
|
<div class="text-center p-4 rounded-lg" :class="getPlayerResultStyle()">
|
||||||
<p class="text-lg font-bold">
|
<p class="text-lg font-bold">
|
||||||
{{
|
{{ report.winner === 'draw' ? t('messagesView.draw') : isPlayerVictory ? t('messagesView.victory') : t('messagesView.defeat') }}
|
||||||
report.winner === 'attacker'
|
|
||||||
? t('messagesView.victory')
|
|
||||||
: report.winner === 'defender'
|
|
||||||
? t('messagesView.defeat')
|
|
||||||
: t('messagesView.draw')
|
|
||||||
}}
|
|
||||||
</p>
|
</p>
|
||||||
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
|
<p v-if="report.rounds" class="text-sm mt-1">{{ t('simulatorView.afterRounds').replace('{rounds}', String(report.rounds)) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,29 +86,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 剩余单位 -->
|
<!-- 剩余单位 -->
|
||||||
<div v-if="report.attackerRemaining || report.defenderRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div v-if="hasAnyRemaining" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<!-- 攻击方剩余 -->
|
<!-- 攻击方剩余 -->
|
||||||
<div v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0" class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.attackerRemaining') }}</p>
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||||
|
<template v-if="report.attackerRemaining && Object.keys(report.attackerRemaining).length > 0">
|
||||||
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
|
<div v-for="(count, shipType) in report.attackerRemaining" :key="shipType">
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
<span class="ml-2 font-medium">{{ count }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 防守方剩余 -->
|
<!-- 防守方剩余 -->
|
||||||
<div
|
<div class="space-y-2">
|
||||||
|
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
|
||||||
|
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
||||||
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
report.defenderRemaining &&
|
report.defenderRemaining &&
|
||||||
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
|
(Object.keys(report.defenderRemaining.fleet || {}).length > 0 ||
|
||||||
Object.keys(report.defenderRemaining.defense || {}).length > 0)
|
Object.keys(report.defenderRemaining.defense || {}).length > 0)
|
||||||
"
|
"
|
||||||
class="space-y-2"
|
|
||||||
>
|
>
|
||||||
<p class="text-sm font-medium text-blue-600 dark:text-blue-400">{{ t('messagesView.defenderRemaining') }}</p>
|
|
||||||
<div class="p-3 bg-muted rounded-lg space-y-1 text-xs">
|
|
||||||
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
|
<div v-for="(count, shipType) in report.defenderRemaining.fleet" :key="shipType">
|
||||||
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
<span class="text-muted-foreground">{{ SHIPS[shipType].name }}:</span>
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
<span class="ml-2 font-medium">{{ count }}</span>
|
||||||
@@ -123,19 +120,19 @@
|
|||||||
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
<span class="text-muted-foreground">{{ DEFENSES[defenseType].name }}:</span>
|
||||||
<span class="ml-2 font-medium">{{ count }}</span>
|
<span class="ml-2 font-medium">{{ count }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="text-muted-foreground">{{ t('messagesView.allDestroyed') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 战利品和残骸 -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<!-- 掠夺资源 -->
|
<!-- 掠夺资源 -->
|
||||||
<div
|
<div
|
||||||
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
|
v-if="report.plunder && (report.plunder.metal > 0 || report.plunder.crystal > 0 || report.plunder.deuterium > 0)"
|
||||||
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
class="p-3 bg-green-50 dark:bg-green-950 rounded-lg"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
|
<p class="text-sm font-medium mb-2 text-green-600 dark:text-green-400">{{ t('messagesView.plunder') }}</p>
|
||||||
<div class="flex flex-wrap gap-3 text-xs">
|
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
||||||
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
|
<span v-if="report.plunder.metal > 0" class="flex items-center gap-1">
|
||||||
<ResourceIcon type="metal" size="sm" />
|
<ResourceIcon type="metal" size="sm" />
|
||||||
{{ formatNumber(report.plunder.metal) }}
|
{{ formatNumber(report.plunder.metal) }}
|
||||||
@@ -154,10 +151,10 @@
|
|||||||
<!-- 残骸场 -->
|
<!-- 残骸场 -->
|
||||||
<div
|
<div
|
||||||
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
|
v-if="report.debrisField && (report.debrisField.metal > 0 || report.debrisField.crystal > 0)"
|
||||||
class="p-3 bg-muted rounded-lg"
|
class="text-center p-4 bg-muted rounded-lg"
|
||||||
>
|
>
|
||||||
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
|
<p class="text-sm font-medium mb-2">{{ t('messagesView.debrisField') }}</p>
|
||||||
<div class="flex flex-wrap gap-3 text-xs">
|
<div class="flex flex-wrap gap-3 text-xs justify-center">
|
||||||
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
|
<span v-if="report.debrisField.metal > 0" class="flex items-center gap-1">
|
||||||
<ResourceIcon type="metal" size="sm" />
|
<ResourceIcon type="metal" size="sm" />
|
||||||
{{ formatNumber(report.debrisField.metal) }}
|
{{ formatNumber(report.debrisField.metal) }}
|
||||||
@@ -172,7 +169,6 @@
|
|||||||
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
|
{{ t('messagesView.moonChance') }}: {{ (report.moonChance * 100).toFixed(1) }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 回合详情 -->
|
<!-- 回合详情 -->
|
||||||
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
|
<div v-if="report.roundDetails && report.roundDetails.length > 0" class="space-y-2">
|
||||||
@@ -301,7 +297,11 @@
|
|||||||
// 获取攻击方星球信息
|
// 获取攻击方星球信息
|
||||||
const attackerPlanet = computed(() => {
|
const attackerPlanet = computed(() => {
|
||||||
if (!props.report) return null
|
if (!props.report) return null
|
||||||
return gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
// 先从玩家星球中查找
|
||||||
|
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.attackerPlanetId)
|
||||||
|
if (playerPlanet) return playerPlanet
|
||||||
|
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||||
|
return Object.values(universeStore.planets).find(p => p.id === props.report!.attackerPlanetId)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取防守方星球信息
|
// 获取防守方星球信息
|
||||||
@@ -310,10 +310,35 @@
|
|||||||
// 先从玩家星球中查找
|
// 先从玩家星球中查找
|
||||||
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
|
const playerPlanet = gameStore.player.planets.find(p => p.id === props.report!.defenderPlanetId)
|
||||||
if (playerPlanet) return playerPlanet
|
if (playerPlanet) return playerPlanet
|
||||||
// 再从宇宙星球地图中查找
|
// 再从宇宙星球地图中查找(包括 NPC 星球)
|
||||||
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
|
return Object.values(universeStore.planets).find(p => p.id === props.report!.defenderPlanetId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 判断玩家是攻击方还是防守方
|
||||||
|
const isPlayerAttacker = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
return gameStore.player.planets.some(p => p.id === props.report!.attackerPlanetId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断玩家是否胜利
|
||||||
|
const isPlayerVictory = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
if (props.report.winner === 'draw') return false
|
||||||
|
// 玩家是攻击方且攻击方胜利,或者玩家是防守方且防守方胜利
|
||||||
|
return (isPlayerAttacker.value && props.report.winner === 'attacker') || (!isPlayerAttacker.value && props.report.winner === 'defender')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否有任何剩余单位需要显示
|
||||||
|
const hasAnyRemaining = computed(() => {
|
||||||
|
if (!props.report) return false
|
||||||
|
const hasAttackerRemaining = props.report.attackerRemaining && Object.keys(props.report.attackerRemaining).length > 0
|
||||||
|
const hasDefenderRemaining =
|
||||||
|
props.report.defenderRemaining &&
|
||||||
|
(Object.keys(props.report.defenderRemaining.fleet || {}).length > 0 ||
|
||||||
|
Object.keys(props.report.defenderRemaining.defense || {}).length > 0)
|
||||||
|
return hasAttackerRemaining || hasDefenderRemaining
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.open,
|
() => props.open,
|
||||||
newValue => {
|
newValue => {
|
||||||
@@ -328,10 +353,11 @@
|
|||||||
emit('update:open', newValue)
|
emit('update:open', newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取胜利者样式
|
// 获取玩家战斗结果样式
|
||||||
const getWinnerStyle = (winner: string) => {
|
const getPlayerResultStyle = () => {
|
||||||
if (winner === 'attacker') return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
if (!props.report) return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||||
if (winner === 'defender') return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
if (props.report.winner === 'draw') return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
||||||
return 'bg-gray-50 dark:bg-gray-950 text-gray-700 dark:text-gray-300'
|
if (isPlayerVictory.value) return 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300'
|
||||||
|
return 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,8 +17,14 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription>
|
||||||
{{ requirementsDialogMessage }}
|
<div class="space-y-2">
|
||||||
|
<div v-for="(req, index) in requirementsDialogItems" :key="index" class="flex items-center gap-2 text-sm">
|
||||||
|
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||||
|
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
||||||
|
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -35,7 +41,7 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
import { BuildingType, TechnologyType } from '@/types/game'
|
import { BuildingType, TechnologyType } from '@/types/game'
|
||||||
import { Lock } from 'lucide-vue-next'
|
import { Lock, Check, X } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -61,7 +67,7 @@
|
|||||||
// AlertDialog 状态
|
// AlertDialog 状态
|
||||||
const requirementsDialogOpen = ref(false)
|
const requirementsDialogOpen = ref(false)
|
||||||
const requirementsDialogTitle = ref('')
|
const requirementsDialogTitle = ref('')
|
||||||
const requirementsDialogMessage = ref('')
|
const requirementsDialogItems = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||||
|
|
||||||
const isUnlocked = computed(() => {
|
const isUnlocked = computed(() => {
|
||||||
// 如果已经建造过(level > 0),则认为已解锁,不显示遮罩
|
// 如果已经建造过(level > 0),则认为已解锁,不显示遮罩
|
||||||
@@ -70,34 +76,32 @@
|
|||||||
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
|
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getRequirementsList = (): string => {
|
const getRequirementsList = (): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
if (!props.requirements || !gameStore.currentPlanet) return ''
|
if (!props.requirements || !gameStore.currentPlanet) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
|
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const buildingType = key as BuildingType
|
const buildingType = key as BuildingType
|
||||||
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
|
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
|
||||||
const name = BUILDINGS.value[buildingType]?.name || buildingType
|
const name = BUILDINGS.value[buildingType]?.name || buildingType
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const techType = key as TechnologyType
|
const techType = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[techType] || 0
|
const currentLevel = gameStore.player.technologies[techType] || 0
|
||||||
const name = TECHNOLOGIES.value[techType]?.name || techType
|
const name = TECHNOLOGIES.value[techType]?.name || techType
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
const showRequirements = () => {
|
const showRequirements = () => {
|
||||||
requirementsDialogTitle.value = t('common.requirementsNotMet')
|
requirementsDialogTitle.value = t('common.requirementsNotMet')
|
||||||
requirementsDialogMessage.value = getRequirementsList()
|
requirementsDialogItems.value = getRequirementsList()
|
||||||
requirementsDialogOpen.value = true
|
requirementsDialogOpen.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
338
src/components/DiplomaticNotifications.vue
Normal file
338
src/components/DiplomaticNotifications.vue
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<template>
|
||||||
|
<Popover v-model:open="isOpen">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="icon" class="relative">
|
||||||
|
<ScrollText class="h-4 w-4" />
|
||||||
|
<Badge
|
||||||
|
v-if="unreadCount > 0"
|
||||||
|
variant="destructive"
|
||||||
|
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
|
||||||
|
>
|
||||||
|
{{ unreadCount }}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-96 p-0" align="end">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="font-semibold">{{ t('diplomacy.notifications') }}</h3>
|
||||||
|
<Button v-if="unreadCount > 0" variant="ghost" size="sm" @click="markAllAsRead">
|
||||||
|
{{ t('diplomacy.markAllRead') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea class="h-96">
|
||||||
|
<Empty v-if="reports.length === 0" class="border-0">
|
||||||
|
<EmptyContent>
|
||||||
|
<ScrollText class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('diplomacy.noReports') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
<div v-else class="divide-y">
|
||||||
|
<div
|
||||||
|
v-for="report in reports"
|
||||||
|
:key="report.id"
|
||||||
|
class="p-3 hover:bg-muted/50 cursor-pointer transition-colors"
|
||||||
|
:class="{ 'bg-primary/5': !report.read }"
|
||||||
|
@click="handleReportClick(report)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- 左侧:事件图标 -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.eventType)" />
|
||||||
|
</div>
|
||||||
|
<!-- 中间:主要信息 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm truncate">{{ report.npcName }}</span>
|
||||||
|
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs flex-shrink-0">
|
||||||
|
{{ getStatusText(report.newStatus) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{{ getEventTypeText(report.eventType) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- 右侧:好感度变化和时间 -->
|
||||||
|
<div class="flex-shrink-0 text-right">
|
||||||
|
<span
|
||||||
|
class="text-sm font-bold block"
|
||||||
|
:class="report.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||||
|
>
|
||||||
|
{{ report.reputationChange >= 0 ? '+' : '' }}{{ report.reputationChange }}
|
||||||
|
</span>
|
||||||
|
<span class="text-[10px] text-muted-foreground">
|
||||||
|
{{ formatRelativeTime((Date.now() - report.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 未读标记 -->
|
||||||
|
<span v-if="!report.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<div v-if="reports.length > 0" class="p-2 border-t">
|
||||||
|
<Button variant="ghost" size="sm" class="w-full" @click="goToDiplomacy">
|
||||||
|
{{ t('diplomacy.viewAll') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<!-- 外交报告详情对话框 -->
|
||||||
|
<Dialog :open="detailDialogOpen" @update:open="detailDialogOpen = $event">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<component
|
||||||
|
v-if="selectedReport"
|
||||||
|
:is="getEventIcon(selectedReport.eventType)"
|
||||||
|
class="h-5 w-5"
|
||||||
|
:class="getEventIconColor(selectedReport.eventType)"
|
||||||
|
/>
|
||||||
|
{{ t('diplomacy.reportDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">
|
||||||
|
{{ t('diplomacy.reportDetails') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="selectedReport" class="space-y-4">
|
||||||
|
<!-- NPC信息 -->
|
||||||
|
<div class="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<h3 class="font-semibold text-lg">{{ selectedReport.npcName }}</h3>
|
||||||
|
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)">
|
||||||
|
{{ getStatusText(selectedReport.newStatus) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatRelativeTime((Date.now() - selectedReport.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 事件描述 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('diplomacy.eventDescription') }}</h4>
|
||||||
|
<p class="text-sm p-3 bg-muted/30 rounded-md">
|
||||||
|
{{
|
||||||
|
selectedReport.messageKey && selectedReport.messageParams
|
||||||
|
? t(selectedReport.messageKey, selectedReport.messageParams)
|
||||||
|
: selectedReport.message
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关系变化 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- 好感度变化 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('diplomacy.reputationChange') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div class="flex items-center justify-between text-sm mb-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.before') }}</span>
|
||||||
|
<span class="font-semibold" :class="getReputationColor(selectedReport.newReputation - selectedReport.reputationChange)">
|
||||||
|
{{ selectedReport.newReputation - selectedReport.reputationChange > 0 ? '+' : ''
|
||||||
|
}}{{ selectedReport.newReputation - selectedReport.reputationChange }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center text-lg font-bold my-1"
|
||||||
|
:class="selectedReport.reputationChange >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"
|
||||||
|
>
|
||||||
|
{{ selectedReport.reputationChange >= 0 ? '+' : '' }}{{ selectedReport.reputationChange }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-sm mt-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.after') }}</span>
|
||||||
|
<span class="font-semibold" :class="getReputationColor(selectedReport.newReputation)">
|
||||||
|
{{ selectedReport.newReputation > 0 ? '+' : '' }}{{ selectedReport.newReputation }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关系状态变化 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('diplomacy.statusChange') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div class="flex items-center justify-between text-sm mb-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.before') }}</span>
|
||||||
|
<Badge :variant="getStatusBadgeVariant(selectedReport.oldStatus)" class="text-xs">
|
||||||
|
{{ getStatusText(selectedReport.oldStatus) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center my-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-sm mt-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.after') }}</span>
|
||||||
|
<Badge :variant="getStatusBadgeVariant(selectedReport.newStatus)" class="text-xs">
|
||||||
|
{{ getStatusText(selectedReport.newStatus) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="detailDialogOpen = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="goToDiplomacyFromDialog">{{ t('diplomacy.viewDiplomacy') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { ScrollText, Gift, Sword, Eye, Trash2, Skull } from 'lucide-vue-next'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||||
|
import type { DiplomaticReport } from '@/types/game'
|
||||||
|
import { formatRelativeTime } from '@/utils/format'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const detailDialogOpen = ref(false)
|
||||||
|
const selectedReport = ref<DiplomaticReport | null>(null)
|
||||||
|
|
||||||
|
const reports = computed(() => {
|
||||||
|
return (gameStore.player.diplomaticReports || []).slice().reverse().slice(0, 20) // 最近20条
|
||||||
|
})
|
||||||
|
|
||||||
|
const unreadCount = computed(() => {
|
||||||
|
return (gameStore.player.diplomaticReports || []).filter(r => !r.read).length
|
||||||
|
})
|
||||||
|
|
||||||
|
const getEventIcon = (eventType: DiplomaticReport['eventType']) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case DiplomaticEventType.GiftResources:
|
||||||
|
return Gift
|
||||||
|
case DiplomaticEventType.Attack:
|
||||||
|
case DiplomaticEventType.AllyAttacked:
|
||||||
|
return Sword
|
||||||
|
case DiplomaticEventType.Spy:
|
||||||
|
return Eye
|
||||||
|
case DiplomaticEventType.StealDebris:
|
||||||
|
return Trash2
|
||||||
|
case DiplomaticEventType.DestroyPlanet:
|
||||||
|
return Skull
|
||||||
|
default:
|
||||||
|
return ScrollText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEventIconColor = (eventType: DiplomaticReport['eventType']) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case DiplomaticEventType.GiftResources:
|
||||||
|
return 'text-green-500'
|
||||||
|
case DiplomaticEventType.Attack:
|
||||||
|
case DiplomaticEventType.DestroyPlanet:
|
||||||
|
return 'text-red-500'
|
||||||
|
case DiplomaticEventType.AllyAttacked:
|
||||||
|
return 'text-orange-500'
|
||||||
|
case DiplomaticEventType.Spy:
|
||||||
|
return 'text-purple-500'
|
||||||
|
case DiplomaticEventType.StealDebris:
|
||||||
|
return 'text-yellow-500'
|
||||||
|
default:
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEventTypeText = (eventType: DiplomaticReport['eventType']) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case DiplomaticEventType.GiftResources:
|
||||||
|
return t('diplomacy.eventType.gift')
|
||||||
|
case DiplomaticEventType.Attack:
|
||||||
|
return t('diplomacy.eventType.attack')
|
||||||
|
case DiplomaticEventType.AllyAttacked:
|
||||||
|
return t('diplomacy.eventType.allyAttacked')
|
||||||
|
case DiplomaticEventType.Spy:
|
||||||
|
return t('diplomacy.eventType.spy')
|
||||||
|
case DiplomaticEventType.StealDebris:
|
||||||
|
return t('diplomacy.eventType.stealDebris')
|
||||||
|
case DiplomaticEventType.DestroyPlanet:
|
||||||
|
return t('diplomacy.eventType.destroyPlanet')
|
||||||
|
default:
|
||||||
|
return t('diplomacy.eventType.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBadgeVariant = (status: RelationStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case RelationStatus.Hostile:
|
||||||
|
return 'destructive'
|
||||||
|
case RelationStatus.Friendly:
|
||||||
|
return 'default'
|
||||||
|
case RelationStatus.Neutral:
|
||||||
|
default:
|
||||||
|
return 'secondary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: RelationStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case RelationStatus.Hostile:
|
||||||
|
return t('diplomacy.status.hostile')
|
||||||
|
case RelationStatus.Friendly:
|
||||||
|
return t('diplomacy.status.friendly')
|
||||||
|
case RelationStatus.Neutral:
|
||||||
|
default:
|
||||||
|
return t('diplomacy.status.neutral')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReputationColor = (reputation: number | null) => {
|
||||||
|
if (reputation === null) return 'text-muted-foreground'
|
||||||
|
if (reputation >= 20) return 'text-green-600 dark:text-green-400'
|
||||||
|
if (reputation <= -20) return 'text-red-600 dark:text-red-400'
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReportClick = (report: DiplomaticReport) => {
|
||||||
|
// 标记为已读
|
||||||
|
report.read = true
|
||||||
|
// 设置选中的报告
|
||||||
|
selectedReport.value = report
|
||||||
|
// 关闭通知面板
|
||||||
|
isOpen.value = true
|
||||||
|
// 打开对话框
|
||||||
|
detailDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
gameStore.player.diplomaticReports?.forEach(report => {
|
||||||
|
report.read = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDiplomacy = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
router.push('/diplomacy')
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDiplomacyFromDialog = () => {
|
||||||
|
const npcId = selectedReport.value?.npcId
|
||||||
|
detailDialogOpen.value = false
|
||||||
|
router.push(npcId ? `/diplomacy?npcId=${npcId}` : '/diplomacy')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
304
src/components/EnemyAlertNotifications.vue
Normal file
304
src/components/EnemyAlertNotifications.vue
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<Popover v-model:open="isOpen">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="icon" class="relative">
|
||||||
|
<Siren class="h-4 w-4" />
|
||||||
|
<Badge
|
||||||
|
v-if="activeAlerts.length > 0"
|
||||||
|
variant="destructive"
|
||||||
|
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs animate-pulse"
|
||||||
|
>
|
||||||
|
{{ activeAlerts.length }}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-96 p-0" align="end">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="font-semibold">{{ t('enemyAlert.title') }}</h3>
|
||||||
|
<Button v-if="activeAlerts.length > 0" variant="ghost" size="sm" @click="markAllAsRead">
|
||||||
|
{{ t('enemyAlert.markAllRead') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea class="h-96">
|
||||||
|
<Empty v-if="activeAlerts.length === 0" class="border-0">
|
||||||
|
<EmptyContent>
|
||||||
|
<Shield class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('enemyAlert.noAlerts') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
<div v-else class="divide-y">
|
||||||
|
<div
|
||||||
|
v-for="alert in activeAlerts"
|
||||||
|
:key="alert.id"
|
||||||
|
class="p-3 hover:bg-muted/50 cursor-pointer transition-colors"
|
||||||
|
:class="{ 'bg-destructive/10': !alert.read }"
|
||||||
|
@click="handleAlertClick(alert)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- 左侧:任务图标 -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<component :is="getMissionIcon(alert.missionType)" class="h-5 w-5" :class="getMissionIconColor(alert.missionType)" />
|
||||||
|
</div>
|
||||||
|
<!-- 中间:主要信息 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm truncate">{{ alert.npcName }}</span>
|
||||||
|
<Badge :variant="getMissionBadgeVariant(alert.missionType)" class="text-xs flex-shrink-0">
|
||||||
|
{{ getMissionTypeText(alert.missionType) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{{ alert.targetPlanetName }} · {{ t('enemyAlert.fleetSize') }}: {{ alert.fleetSize }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- 右侧:倒计时 -->
|
||||||
|
<div class="flex-shrink-0 text-right">
|
||||||
|
<span class="text-sm font-bold block" :class="getRemainingTimeColor(alert)">
|
||||||
|
{{ formatRemainingTime(alert) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 未读标记 -->
|
||||||
|
<span v-if="!alert.read" class="h-2 w-2 rounded-full bg-destructive flex-shrink-0 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<div v-if="activeAlerts.length > 0" class="p-2 border-t">
|
||||||
|
<Button variant="ghost" size="sm" class="w-full" @click="goToFleet">
|
||||||
|
{{ t('enemyAlert.viewFleet') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<!-- 警报详情对话框 -->
|
||||||
|
<Dialog :open="detailDialogOpen" @update:open="detailDialogOpen = $event">
|
||||||
|
<DialogContent class="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<component
|
||||||
|
v-if="selectedAlert"
|
||||||
|
:is="getMissionIcon(selectedAlert.missionType)"
|
||||||
|
class="h-5 w-5"
|
||||||
|
:class="getMissionIconColor(selectedAlert.missionType)"
|
||||||
|
/>
|
||||||
|
{{ t('enemyAlert.alertDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">
|
||||||
|
{{ t('enemyAlert.alertDetails') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="selectedAlert" class="space-y-4">
|
||||||
|
<!-- 敌方信息 -->
|
||||||
|
<div class="flex items-center gap-3 p-4 bg-destructive/10 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<h3 class="font-semibold text-lg">{{ selectedAlert.npcName }}</h3>
|
||||||
|
<Badge :variant="getMissionBadgeVariant(selectedAlert.missionType)">
|
||||||
|
{{ getMissionTypeText(selectedAlert.missionType) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ t('enemyAlert.fleetSize') }}: {{ selectedAlert.fleetSize }} {{ t('enemyAlert.ships') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 目标信息 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('enemyAlert.targetInfo') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md flex items-center gap-2">
|
||||||
|
<Globe class="h-4 w-4 text-blue-500" />
|
||||||
|
<span class="font-medium">{{ selectedAlert.targetPlanetName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 到达时间 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('enemyAlert.arrivalTime') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">{{ t('enemyAlert.countdown') }}</span>
|
||||||
|
<span class="font-bold text-lg" :class="getRemainingTimeColor(selectedAlert)">
|
||||||
|
{{ formatRemainingTime(selectedAlert) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
{{ formatDate(selectedAlert.arrivalTime) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 警告提示 -->
|
||||||
|
<div class="p-3 bg-destructive/10 rounded-md border border-destructive/20">
|
||||||
|
<p class="text-sm text-destructive dark:text-red-400">
|
||||||
|
{{ getMissionWarningText(selectedAlert.missionType) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="detailDialogOpen = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="goToMessagesFromDialog">{{ t('enemyAlert.viewMessages') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
import { Siren, Eye, Sword, Shield, Globe } from 'lucide-vue-next'
|
||||||
|
import { MissionType } from '@/types/game'
|
||||||
|
import type { IncomingFleetAlert } from '@/types/game'
|
||||||
|
import { formatDate, formatTime } from '@/utils/format'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const detailDialogOpen = ref(false)
|
||||||
|
const selectedAlert = ref<IncomingFleetAlert | null>(null)
|
||||||
|
const currentTime = ref(Date.now())
|
||||||
|
let timeInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
// 启动计时器,用于实时更新倒计时
|
||||||
|
onMounted(() => {
|
||||||
|
timeInterval = setInterval(() => {
|
||||||
|
currentTime.value = Date.now()
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timeInterval) {
|
||||||
|
clearInterval(timeInterval)
|
||||||
|
timeInterval = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取活跃的警报(未到达的)
|
||||||
|
const activeAlerts = computed(() => {
|
||||||
|
const now = currentTime.value
|
||||||
|
return (gameStore.player.incomingFleetAlerts || [])
|
||||||
|
.filter(alert => alert.arrivalTime > now)
|
||||||
|
.sort((a, b) => a.arrivalTime - b.arrivalTime) // 按到达时间排序
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取任务类型图标
|
||||||
|
const getMissionIcon = (missionType: MissionType) => {
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Spy:
|
||||||
|
return Eye
|
||||||
|
case MissionType.Attack:
|
||||||
|
return Sword
|
||||||
|
default:
|
||||||
|
return Siren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务类型图标颜色
|
||||||
|
const getMissionIconColor = (missionType: MissionType) => {
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Spy:
|
||||||
|
return 'text-purple-500'
|
||||||
|
case MissionType.Attack:
|
||||||
|
return 'text-red-500'
|
||||||
|
default:
|
||||||
|
return 'text-yellow-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务类型Badge样式
|
||||||
|
const getMissionBadgeVariant = (missionType: MissionType): 'destructive' | 'secondary' => {
|
||||||
|
return missionType === MissionType.Attack ? 'destructive' : 'secondary'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务类型文本
|
||||||
|
const getMissionTypeText = (missionType: MissionType) => {
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Spy:
|
||||||
|
return t('enemyAlert.missionType.spy')
|
||||||
|
case MissionType.Attack:
|
||||||
|
return t('enemyAlert.missionType.attack')
|
||||||
|
default:
|
||||||
|
return t('enemyAlert.missionType.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务警告文本
|
||||||
|
const getMissionWarningText = (missionType: MissionType) => {
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Spy:
|
||||||
|
return t('enemyAlert.warning.spy')
|
||||||
|
case MissionType.Attack:
|
||||||
|
return t('enemyAlert.warning.attack')
|
||||||
|
default:
|
||||||
|
return t('enemyAlert.warning.unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化剩余时间
|
||||||
|
const formatRemainingTime = (alert: IncomingFleetAlert) => {
|
||||||
|
const remaining = Math.max(0, Math.floor((alert.arrivalTime - currentTime.value) / 1000))
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return t('enemyAlert.arrived')
|
||||||
|
}
|
||||||
|
return formatTime(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剩余时间颜色
|
||||||
|
const getRemainingTimeColor = (alert: IncomingFleetAlert) => {
|
||||||
|
const remaining = alert.arrivalTime - currentTime.value
|
||||||
|
if (remaining <= 0) return 'text-red-600 dark:text-red-400 font-bold' // 已到达
|
||||||
|
if (remaining < 60000) return 'text-red-600 dark:text-red-400' // < 1分钟
|
||||||
|
if (remaining < 300000) return 'text-orange-600 dark:text-orange-400' // < 5分钟
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理警报点击
|
||||||
|
const handleAlertClick = (alert: IncomingFleetAlert) => {
|
||||||
|
alert.read = true
|
||||||
|
selectedAlert.value = alert
|
||||||
|
isOpen.value = true
|
||||||
|
// 打开对话框
|
||||||
|
detailDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记所有为已读
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
gameStore.player.incomingFleetAlerts?.forEach(alert => {
|
||||||
|
alert.read = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到舰队页面
|
||||||
|
const goToFleet = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
router.push('/fleet')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从对话框跳转到消息页面
|
||||||
|
const goToMessagesFromDialog = () => {
|
||||||
|
detailDialogOpen.value = false
|
||||||
|
router.push('/messages')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开弹窗(供外部调用)
|
||||||
|
const open = () => {
|
||||||
|
isOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
</script>
|
||||||
86
src/components/HintToast.vue
Normal file
86
src/components/HintToast.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="-translate-y-4 opacity-0"
|
||||||
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
|
leave-to-class="-translate-y-4 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isHintVisible && currentHint"
|
||||||
|
class="fixed top-2 right-2 max-w-[280px] sm:top-4 sm:right-4 sm:max-w-xs z-50 pointer-events-auto"
|
||||||
|
>
|
||||||
|
<div class="bg-card border rounded-lg shadow-lg p-3" role="alert" aria-live="polite">
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<component :is="getIcon(currentHint.icon)" class="h-4 w-4 text-primary flex-shrink-0" />
|
||||||
|
<h4 class="font-medium text-sm">{{ t(currentHint.titleKey) }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<p class="text-sm text-muted-foreground mb-3 line-clamp-3">{{ t(currentHint.messageKey) }}</p>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<Button size="sm" class="text-xs h-7" @click="handleDismiss">
|
||||||
|
{{ t('hints.dontShowAgain') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useHints } from '@/composables/useHints'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Home,
|
||||||
|
Building,
|
||||||
|
FlaskConical,
|
||||||
|
Rocket,
|
||||||
|
Plane,
|
||||||
|
Globe,
|
||||||
|
Handshake,
|
||||||
|
Mail,
|
||||||
|
Shield,
|
||||||
|
Lightbulb,
|
||||||
|
Users,
|
||||||
|
Swords,
|
||||||
|
Settings,
|
||||||
|
Wand2
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { currentHint, isHintVisible, dismissHint } = useHints()
|
||||||
|
|
||||||
|
// 图标名称到组件的映射
|
||||||
|
const iconMap: Record<string, Component> = {
|
||||||
|
home: Home,
|
||||||
|
building: Building,
|
||||||
|
flask: FlaskConical,
|
||||||
|
rocket: Rocket,
|
||||||
|
plane: Plane,
|
||||||
|
globe: Globe,
|
||||||
|
handshake: Handshake,
|
||||||
|
mail: Mail,
|
||||||
|
shield: Shield,
|
||||||
|
users: Users,
|
||||||
|
swords: Swords,
|
||||||
|
settings: Settings,
|
||||||
|
wand: Wand2
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcon = (iconName?: string) => {
|
||||||
|
if (!iconName) return Lightbulb
|
||||||
|
return iconMap[iconName] || Lightbulb
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不再显示 - 永久关闭
|
||||||
|
const handleDismiss = () => {
|
||||||
|
dismissHint(true)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,67 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="alerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
|
<div v-if="activeAlerts.length > 0" class="bg-destructive/10 border-b border-destructive/20">
|
||||||
<div class="px-4 sm:px-6 py-2 space-y-2">
|
<div class="px-4 sm:px-6 py-2 flex items-center justify-between gap-3">
|
||||||
<div
|
<!-- 警告图标和汇总信息 -->
|
||||||
v-for="alert in alerts"
|
|
||||||
:key="alert.id"
|
|
||||||
class="flex items-center justify-between gap-3 bg-destructive/5 rounded-lg px-3 py-2 border border-destructive/20"
|
|
||||||
>
|
|
||||||
<!-- 警告图标和信息 -->
|
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
|
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 animate-pulse" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-semibold text-destructive truncate">
|
<p class="text-sm font-semibold text-destructive">
|
||||||
<template v-if="alert.missionType === 'spy'">
|
{{ getAlertSummary() }}
|
||||||
{{ t('alerts.npcSpyIncoming') }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="alert.missionType === 'attack'">
|
|
||||||
{{ t('alerts.npcAttackIncoming') }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ t('alerts.npcFleetIncoming') }}
|
|
||||||
</template>
|
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted-foreground truncate">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ alert.npcName }} → {{ alert.targetPlanetName }}
|
{{ t('enemyAlert.countdown') }}: {{ formatTimeRemaining(nearestAlert?.arrivalTime || 0) }}
|
||||||
<template v-if="alert.missionType === 'attack'">({{ alert.fleetSize }} {{ t('alerts.ships') }})</template>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 倒计时 -->
|
<!-- 查看按钮 -->
|
||||||
<div class="flex items-center gap-2 flex-shrink-0">
|
<Button @click="openAlertPanel" variant="outline" size="sm" class="flex-shrink-0">
|
||||||
<div class="text-right">
|
{{ t('common.view') }}
|
||||||
<p class="text-xs font-mono text-destructive">
|
|
||||||
{{ formatTimeRemaining(alert.arrivalTime) }}
|
|
||||||
</p>
|
|
||||||
<p class="text-[10px] text-muted-foreground">
|
|
||||||
{{ formatTime(alert.arrivalTime) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button @click="markAsRead(alert)" variant="ghost" size="sm" class="h-6 w-6 p-0">
|
|
||||||
<X class="h-3 w-3" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import type { IncomingFleetAlert } from '@/types/game'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AlertTriangle, X } from 'lucide-vue-next'
|
import { AlertTriangle } from 'lucide-vue-next'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { MissionType } from '@/types/game'
|
||||||
const props = defineProps<{
|
|
||||||
alerts: IncomingFleetAlert[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'markAsRead', alert: IncomingFleetAlert): void
|
(e: 'openPanel'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const gameStore = useGameStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 强制更新倒计时
|
// 强制更新倒计时
|
||||||
@@ -78,6 +51,46 @@
|
|||||||
if (updateInterval) clearInterval(updateInterval)
|
if (updateInterval) clearInterval(updateInterval)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取活跃的警报(未到达的)
|
||||||
|
const activeAlerts = computed(() => {
|
||||||
|
return (gameStore.player.incomingFleetAlerts || [])
|
||||||
|
.filter(alert => alert.arrivalTime > now.value)
|
||||||
|
.sort((a, b) => a.arrivalTime - b.arrivalTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 最近的警报
|
||||||
|
const nearestAlert = computed(() => activeAlerts.value[0] || null)
|
||||||
|
|
||||||
|
// 统计各类型警报数量
|
||||||
|
const alertCounts = computed(() => {
|
||||||
|
const counts = { spy: 0, attack: 0, other: 0 }
|
||||||
|
activeAlerts.value.forEach(alert => {
|
||||||
|
if (alert.missionType === MissionType.Spy) {
|
||||||
|
counts.spy++
|
||||||
|
} else if (alert.missionType === MissionType.Attack) {
|
||||||
|
counts.attack++
|
||||||
|
} else {
|
||||||
|
counts.other++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return counts
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成警报汇总文本
|
||||||
|
const getAlertSummary = (): string => {
|
||||||
|
const parts: string[] = []
|
||||||
|
if (alertCounts.value.attack > 0) {
|
||||||
|
parts.push(`${alertCounts.value.attack} ${t('enemyAlert.missionType.attack')}`)
|
||||||
|
}
|
||||||
|
if (alertCounts.value.spy > 0) {
|
||||||
|
parts.push(`${alertCounts.value.spy} ${t('enemyAlert.missionType.spy')}`)
|
||||||
|
}
|
||||||
|
if (alertCounts.value.other > 0) {
|
||||||
|
parts.push(`${alertCounts.value.other} ${t('enemyAlert.missionType.unknown')}`)
|
||||||
|
}
|
||||||
|
return t('alerts.incomingFleets', { count: activeAlerts.value.length }) + ': ' + parts.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
const formatTimeRemaining = (arrivalTime: number): string => {
|
const formatTimeRemaining = (arrivalTime: number): string => {
|
||||||
const remaining = Math.max(0, arrivalTime - now.value)
|
const remaining = Math.max(0, arrivalTime - now.value)
|
||||||
const seconds = Math.floor((remaining / 1000) % 60)
|
const seconds = Math.floor((remaining / 1000) % 60)
|
||||||
@@ -90,12 +103,7 @@
|
|||||||
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (timestamp: number): string => {
|
const openAlertPanel = () => {
|
||||||
const date = new Date(timestamp)
|
emit('openPanel')
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const markAsRead = (alert: IncomingFleetAlert) => {
|
|
||||||
emit('markAsRead', alert)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -175,15 +175,21 @@
|
|||||||
<CardContent class="space-y-2">
|
<CardContent class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
<span class="text-muted-foreground">{{ t('resources.metal') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="totalStats.metal" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="totalStats.metal" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
<span class="text-muted-foreground">{{ t('resources.crystal') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="totalStats.crystal" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="totalStats.crystal" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
<span class="text-muted-foreground">{{ t('resources.deuterium') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="totalStats.deuterium" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="totalStats.deuterium" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -305,11 +311,15 @@
|
|||||||
class="flex items-center justify-between text-sm"
|
class="flex items-center justify-between text-sm"
|
||||||
>
|
>
|
||||||
<span class="text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}:</span>
|
<span class="text-muted-foreground">{{ t(`resources.${resourceType.key}`) }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="unitCost[resourceType.key]" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="unitCost[resourceType.key]" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm pt-2 border-t">
|
<div class="flex items-center justify-between text-sm pt-2 border-t">
|
||||||
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
|
<span class="text-muted-foreground">{{ t('player.points') }}:</span>
|
||||||
<span class="font-bold text-primary"><NumberWithTooltip :value="pointsPerUnit" /></span>
|
<span class="font-bold text-primary">
|
||||||
|
<NumberWithTooltip :value="pointsPerUnit" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -341,15 +351,21 @@
|
|||||||
<div class="space-y-1 text-sm">
|
<div class="space-y-1 text-sm">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>{{ t('resources.metal') }}:</span>
|
<span>{{ t('resources.metal') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="batchCost.metal" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="batchCost.metal" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>{{ t('resources.crystal') }}:</span>
|
<span>{{ t('resources.crystal') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="batchCost.crystal" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="batchCost.crystal" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>{{ t('resources.deuterium') }}:</span>
|
<span>{{ t('resources.deuterium') }}:</span>
|
||||||
<span class="font-medium"><NumberWithTooltip :value="batchCost.deuterium" /></span>
|
<span class="font-medium">
|
||||||
|
<NumberWithTooltip :value="batchCost.deuterium" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,7 +486,7 @@
|
|||||||
const showFleetStorageColumn = computed(() => {
|
const showFleetStorageColumn = computed(() => {
|
||||||
if (props.type === 'building') {
|
if (props.type === 'building') {
|
||||||
const buildingType = props.itemType as BuildingType
|
const buildingType = props.itemType as BuildingType
|
||||||
return buildingType === 'shipyard'
|
return buildingType === 'shipyard' || buildingType === 'hangar'
|
||||||
} else if (props.type === 'technology') {
|
} else if (props.type === 'technology') {
|
||||||
const techType = props.itemType as TechnologyType
|
const techType = props.itemType as TechnologyType
|
||||||
return techType === 'computerTechnology'
|
return techType === 'computerTechnology'
|
||||||
@@ -662,43 +678,87 @@
|
|||||||
const storageBonus = 1 + (activeBonuses.value.storageCapacityBonus || 0) / 100
|
const storageBonus = 1 + (activeBonuses.value.storageCapacityBonus || 0) / 100
|
||||||
const baseCapacity = 10000
|
const baseCapacity = 10000
|
||||||
|
|
||||||
if (buildingType === 'metalMine') {
|
// Building calculation configuration
|
||||||
production = Math.floor(1500 * level * Math.pow(1.5, level) * resourceBonus)
|
const buildingCalculations: Record<
|
||||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
string,
|
||||||
} else if (buildingType === 'crystalMine') {
|
(level: number) => Partial<{
|
||||||
production = Math.floor(1000 * level * Math.pow(1.5, level) * resourceBonus)
|
production: number
|
||||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
consumption: number
|
||||||
} else if (buildingType === 'deuteriumSynthesizer') {
|
capacity: number
|
||||||
production = Math.floor(500 * level * Math.pow(1.5, level) * resourceBonus)
|
fleetStorage: number
|
||||||
consumption = Math.floor(10 * level * Math.pow(1.1, level))
|
spaceBonus: number
|
||||||
} else if (buildingType === 'solarPlant') {
|
buildSpeedBonus: number
|
||||||
production = Math.floor(50 * level * Math.pow(1.1, level) * energyBonus)
|
researchSpeedBonus: number
|
||||||
} else if (buildingType === 'metalStorage') {
|
}>
|
||||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
> = {
|
||||||
} else if (buildingType === 'crystalStorage') {
|
metalMine: lvl => ({
|
||||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
production: Math.floor(1500 * lvl * Math.pow(1.5, lvl) * resourceBonus),
|
||||||
} else if (buildingType === 'deuteriumTank') {
|
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
|
||||||
capacity = Math.floor(baseCapacity * Math.pow(2, level) * storageBonus)
|
}),
|
||||||
} else if (buildingType === 'darkMatterCollector') {
|
crystalMine: lvl => ({
|
||||||
capacity = 1000 + level * 100
|
production: Math.floor(1000 * lvl * Math.pow(1.5, lvl) * resourceBonus),
|
||||||
production = Math.floor(25 * level * Math.pow(1.5, level))
|
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
|
||||||
} else if (buildingType === 'darkMatterTank') {
|
}),
|
||||||
const darkMatterBaseCapacity = 1000
|
deuteriumSynthesizer: lvl => ({
|
||||||
capacity = Math.floor(darkMatterBaseCapacity * Math.pow(2, level) * storageBonus)
|
production: Math.floor(500 * lvl * Math.pow(1.5, lvl) * resourceBonus),
|
||||||
} else if (buildingType === 'fusionReactor') {
|
consumption: Math.floor(10 * lvl * Math.pow(1.1, lvl))
|
||||||
production = Math.floor(150 * level * Math.pow(1.15, level))
|
}),
|
||||||
} else if (buildingType === 'shipyard') {
|
solarPlant: lvl => ({
|
||||||
fleetStorage = 1000 * level
|
production: Math.floor(50 * lvl * Math.pow(1.1, lvl) * energyBonus)
|
||||||
} else if (buildingType === 'terraformer') {
|
}),
|
||||||
spaceBonus = 30
|
metalStorage: lvl => ({
|
||||||
} else if (buildingType === 'lunarBase') {
|
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||||
spaceBonus = 30
|
}),
|
||||||
} else if (buildingType === 'roboticsFactory') {
|
crystalStorage: lvl => ({
|
||||||
buildSpeedBonus = level
|
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||||
} else if (buildingType === 'naniteFactory') {
|
}),
|
||||||
buildSpeedBonus = level * 2
|
deuteriumTank: lvl => ({
|
||||||
} else if (buildingType === 'researchLab') {
|
capacity: Math.floor(baseCapacity * Math.pow(2, lvl) * storageBonus)
|
||||||
researchSpeedBonus = level
|
}),
|
||||||
|
darkMatterCollector: lvl => ({
|
||||||
|
capacity: 1000 + lvl * 100,
|
||||||
|
production: Math.floor(25 * lvl * Math.pow(1.5, lvl))
|
||||||
|
}),
|
||||||
|
darkMatterTank: lvl => ({
|
||||||
|
capacity: Math.floor(1000 * Math.pow(2, lvl) * storageBonus)
|
||||||
|
}),
|
||||||
|
fusionReactor: lvl => ({
|
||||||
|
production: Math.floor(150 * lvl * Math.pow(1.15, lvl))
|
||||||
|
}),
|
||||||
|
shipyard: lvl => ({
|
||||||
|
fleetStorage: 1000 * lvl
|
||||||
|
}),
|
||||||
|
hangar: lvl => ({
|
||||||
|
fleetStorage: 500 * lvl
|
||||||
|
}),
|
||||||
|
terraformer: () => ({
|
||||||
|
spaceBonus: 30
|
||||||
|
}),
|
||||||
|
lunarBase: () => ({
|
||||||
|
spaceBonus: 30
|
||||||
|
}),
|
||||||
|
roboticsFactory: lvl => ({
|
||||||
|
buildSpeedBonus: lvl
|
||||||
|
}),
|
||||||
|
naniteFactory: lvl => ({
|
||||||
|
buildSpeedBonus: lvl * 2
|
||||||
|
}),
|
||||||
|
researchLab: lvl => ({
|
||||||
|
researchSpeedBonus: lvl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply calculations if configuration exists
|
||||||
|
const calc = buildingCalculations[buildingType]
|
||||||
|
if (calc) {
|
||||||
|
const result = calc(level)
|
||||||
|
production = result.production ?? production
|
||||||
|
consumption = result.consumption ?? consumption
|
||||||
|
capacity = result.capacity ?? capacity
|
||||||
|
fleetStorage = result.fleetStorage ?? fleetStorage
|
||||||
|
spaceBonus = result.spaceBonus ?? spaceBonus
|
||||||
|
buildSpeedBonus = result.buildSpeedBonus ?? buildSpeedBonus
|
||||||
|
researchSpeedBonus = result.researchSpeedBonus ?? researchSpeedBonus
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = pointsLogic.calculateBuildingPoints(buildingType, level - 1, level)
|
const points = pointsLogic.calculateBuildingPoints(buildingType, level - 1, level)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="rounded-lg transition-shadow duration-300">
|
||||||
<Card>
|
<Card>
|
||||||
<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">
|
||||||
{{ npc.name }}
|
{{ npc.name }}
|
||||||
|
<span v-if="npc.note" class="text-muted-foreground font-normal">({{ npc.note }})</span>
|
||||||
<Badge :variant="statusBadgeVariant">
|
<Badge :variant="statusBadgeVariant">
|
||||||
{{ statusText }}
|
{{ statusText }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -14,6 +16,16 @@
|
|||||||
<span v-if="npc.allies && npc.allies.length > 0" class="ml-2">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
<span v-if="npc.allies && npc.allies.length > 0" class="ml-2">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 编辑备注按钮 -->
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
@click="openNoteDialog"
|
||||||
|
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||||
|
>
|
||||||
|
<Pencil class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
@@ -53,7 +65,14 @@
|
|||||||
<div v-if="npc.allies && npc.allies.length > 0" class="pt-2 border-t">
|
<div v-if="npc.allies && npc.allies.length > 0" class="pt-2 border-t">
|
||||||
<p class="text-sm text-muted-foreground mb-2">{{ t('diplomacy.alliedWith') }}:</p>
|
<p class="text-sm text-muted-foreground mb-2">{{ t('diplomacy.alliedWith') }}:</p>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<Badge v-for="allyId in npc.allies.slice(0, 3)" :key="allyId" variant="outline" class="text-xs">
|
<Badge
|
||||||
|
v-for="allyId in npc.allies.slice(0, 3)"
|
||||||
|
:key="allyId"
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
:class="getAllyBorderClass(allyId)"
|
||||||
|
@click="scrollToAlly(allyId)"
|
||||||
|
>
|
||||||
{{ getAllyName(allyId) }}
|
{{ getAllyName(allyId) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-if="npc.allies.length > 3" variant="outline" class="text-xs">
|
<Badge v-if="npc.allies.length > 3" variant="outline" class="text-xs">
|
||||||
@@ -80,25 +99,48 @@
|
|||||||
<div class="flex items-center gap-2 text-xs">
|
<div class="flex items-center gap-2 text-xs">
|
||||||
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
|
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
|
||||||
<span>{{ getEventText(recentEvent.reason) }}</span>
|
<span>{{ getEventText(recentEvent.reason) }}</span>
|
||||||
<span class="text-muted-foreground">{{ formatTime(Date.now() - recentEvent.timestamp) }} {{ t('diplomacy.ago') }}</span>
|
<span class="text-muted-foreground">
|
||||||
|
{{ formatRelativeTime((Date.now() - recentEvent.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- 备注编辑对话框 -->
|
||||||
|
<Dialog v-model:open="noteDialogOpen">
|
||||||
|
<DialogContent class="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote') }}</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">{{ t('diplomacy.note') }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="py-4">
|
||||||
|
<Input v-model="noteInput" :placeholder="t('diplomacy.notePlaceholder')" @keyup.enter="saveNote" />
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="noteDialogOpen = false">{{ t('common.cancel') }}</Button>
|
||||||
|
<Button @click="saveNote">{{ t('common.save') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useNPCStore } from '@/stores/npcStore'
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
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 { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Gift, Globe, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Gift, Globe, Sword, Eye, Trash2, Pencil } from 'lucide-vue-next'
|
||||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||||
import type { DiplomaticRelation, NPC } from '@/types/game'
|
import type { DiplomaticRelation, NPC } from '@/types/game'
|
||||||
import { formatTime } from '@/utils/format'
|
import { formatRelativeTime } from '@/utils/format'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
npc: NPC
|
npc: NPC
|
||||||
@@ -107,8 +149,28 @@
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
|
const gameStore = useGameStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 备注对话框状态
|
||||||
|
const noteDialogOpen = ref(false)
|
||||||
|
const noteInput = ref('')
|
||||||
|
|
||||||
|
// 打开备注对话框
|
||||||
|
const openNoteDialog = () => {
|
||||||
|
noteInput.value = props.npc.note || ''
|
||||||
|
noteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存备注
|
||||||
|
const saveNote = () => {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === props.npc.id)
|
||||||
|
if (npc) {
|
||||||
|
npc.note = noteInput.value.trim() || undefined
|
||||||
|
}
|
||||||
|
noteDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
// 好感度值
|
// 好感度值
|
||||||
const reputation = computed(() => props.relation?.reputation || 0)
|
const reputation = computed(() => props.relation?.reputation || 0)
|
||||||
|
|
||||||
@@ -155,7 +217,26 @@
|
|||||||
// 获取盟友名称
|
// 获取盟友名称
|
||||||
const getAllyName = (allyId: string) => {
|
const getAllyName = (allyId: string) => {
|
||||||
const ally = npcStore.npcs.find(n => n.id === allyId)
|
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||||
return ally?.name || allyId.substring(0, 8)
|
if (!ally) return allyId.substring(0, 8)
|
||||||
|
return ally.note ? `${ally.name}(${ally.note})` : ally.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取盟友与玩家的外交关系状态对应的边框样式
|
||||||
|
const getAllyBorderClass = (allyId: string) => {
|
||||||
|
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||||
|
if (!ally) return ''
|
||||||
|
|
||||||
|
const allyRelation = ally.relations?.[gameStore.player.id]
|
||||||
|
if (!allyRelation) return '' // 无关系,使用默认边框
|
||||||
|
|
||||||
|
switch (allyRelation.status) {
|
||||||
|
case RelationStatus.Friendly:
|
||||||
|
return 'border-green-500 dark:border-green-400'
|
||||||
|
case RelationStatus.Hostile:
|
||||||
|
return 'border-red-500 dark:border-red-400'
|
||||||
|
default:
|
||||||
|
return '' // 中立,使用默认边框
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取事件图标
|
// 获取事件图标
|
||||||
@@ -229,4 +310,12 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到盟友卡片
|
||||||
|
const scrollToAlly = (allyId: string) => {
|
||||||
|
// 触发父组件的滚动事件
|
||||||
|
// 通过emit通知父组件滚动到指定的NPC卡片
|
||||||
|
const event = new CustomEvent('scrollToNpc', { detail: { npcId: allyId }, bubbles: true })
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
323
src/components/NpcRelationRow.vue
Normal file
323
src/components/NpcRelationRow.vue
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg transition-shadow duration-300">
|
||||||
|
<div class="p-3 rounded-lg border hover:bg-accent/50 transition-colors cursor-pointer" @click="toggleExpand">
|
||||||
|
<!-- 桌面端:单行布局 -->
|
||||||
|
<div class="hidden sm:flex items-center gap-3">
|
||||||
|
<!-- 状态指示器 -->
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': status === RelationStatus.Friendly,
|
||||||
|
'bg-red-500': status === RelationStatus.Hostile,
|
||||||
|
'bg-gray-400': status === RelationStatus.Neutral
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 名称和备注 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium truncate">{{ npc.name }}</span>
|
||||||
|
<span v-if="npc.note" class="text-muted-foreground text-sm truncate">({{ npc.note }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground">
|
||||||
|
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
|
||||||
|
<span v-if="npc.allies && npc.allies.length > 0">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 好感度 -->
|
||||||
|
<div class="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<div class="w-16 h-1.5 bg-muted rounded-full overflow-hidden relative">
|
||||||
|
<div v-if="reputation < 0" class="h-full bg-red-500 absolute right-1/2" :style="{ width: `${Math.abs(reputation) / 2}%` }" />
|
||||||
|
<div v-if="reputation > 0" class="h-full bg-green-500 absolute left-1/2" :style="{ width: `${reputation / 2}%` }" />
|
||||||
|
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-border" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium w-10 text-right" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click.stop="handleGiftResources" :title="t('diplomacy.actions.gift')">
|
||||||
|
<Gift class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-8 w-8" @click.stop="handleViewPlanets" :title="t('diplomacy.actions.viewPlanets')">
|
||||||
|
<Globe class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click.stop="openNoteDialog"
|
||||||
|
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||||
|
>
|
||||||
|
<Pencil class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform" :class="{ 'rotate-180': isExpanded }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端:两行布局 -->
|
||||||
|
<div class="sm:hidden space-y-2">
|
||||||
|
<!-- 第一行:状态、名称、展开箭头 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': status === RelationStatus.Friendly,
|
||||||
|
'bg-red-500': status === RelationStatus.Hostile,
|
||||||
|
'bg-gray-400': status === RelationStatus.Neutral
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="font-medium truncate">{{ npc.name }}</span>
|
||||||
|
<span v-if="npc.note" class="text-muted-foreground text-sm ml-1">({{ npc.note }})</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown class="h-4 w-4 text-muted-foreground transition-transform flex-shrink-0" :class="{ 'rotate-180': isExpanded }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:星球数、好感度、操作按钮 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-xs text-muted-foreground">
|
||||||
|
{{ npc.planets.length }} {{ t('diplomacy.planets') }}
|
||||||
|
<span v-if="npc.allies && npc.allies.length > 0">· {{ npc.allies.length }} {{ t('diplomacy.allies') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- 好感度数值 -->
|
||||||
|
<span class="text-xs font-medium mr-1" :class="reputationColor">{{ reputation > 0 ? '+' : '' }}{{ reputation }}</span>
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click.stop="handleGiftResources" :title="t('diplomacy.actions.gift')">
|
||||||
|
<Gift class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click.stop="handleViewPlanets" :title="t('diplomacy.actions.viewPlanets')">
|
||||||
|
<Globe class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click.stop="openNoteDialog"
|
||||||
|
:title="npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote')"
|
||||||
|
>
|
||||||
|
<Pencil class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开详情 -->
|
||||||
|
<div v-if="isExpanded" class="ml-5 pl-3 border-l-2 border-muted py-2 space-y-2">
|
||||||
|
<!-- 盟友信息 -->
|
||||||
|
<div v-if="npc.allies && npc.allies.length > 0" class="flex items-center gap-2 flex-wrap">
|
||||||
|
<span class="text-xs text-muted-foreground">{{ t('diplomacy.alliedWith') }}:</span>
|
||||||
|
<Badge
|
||||||
|
v-for="allyId in npc.allies.slice(0, 5)"
|
||||||
|
:key="allyId"
|
||||||
|
variant="outline"
|
||||||
|
class="text-xs cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
:class="getAllyBorderClass(allyId)"
|
||||||
|
@click="scrollToAlly(allyId)"
|
||||||
|
>
|
||||||
|
{{ getAllyName(allyId) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge v-if="npc.allies.length > 5" variant="outline" class="text-xs">+{{ npc.allies.length - 5 }} {{ t('diplomacy.more') }}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近活动 -->
|
||||||
|
<div v-if="recentEvent" class="flex items-center gap-2 text-xs">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.lastEvent') }}:</span>
|
||||||
|
<component :is="getEventIcon(recentEvent.reason)" class="h-3 w-3" />
|
||||||
|
<span>{{ getEventText(recentEvent.reason) }}</span>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
{{ formatRelativeTime((Date.now() - recentEvent.timestamp) / 1000, t) }}{{ t('diplomacy.ago') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备注编辑对话框 -->
|
||||||
|
<Dialog v-model:open="noteDialogOpen">
|
||||||
|
<DialogContent class="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ npc.note ? t('diplomacy.actions.editNote') : t('diplomacy.actions.addNote') }}</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">{{ t('diplomacy.note') }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="py-4">
|
||||||
|
<Input v-model="noteInput" :placeholder="t('diplomacy.notePlaceholder')" @keyup.enter="saveNote" />
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="noteDialogOpen = false">{{ t('common.cancel') }}</Button>
|
||||||
|
<Button @click="saveNote">{{ t('common.save') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Gift, Globe, Pencil, ChevronDown, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
||||||
|
import type { DiplomaticRelation, NPC } from '@/types/game'
|
||||||
|
import { formatRelativeTime } from '@/utils/format'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
npc: NPC
|
||||||
|
relation?: DiplomaticRelation
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const npcStore = useNPCStore()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 展开状态
|
||||||
|
const isExpanded = ref(false)
|
||||||
|
const toggleExpand = () => {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备注对话框状态
|
||||||
|
const noteDialogOpen = ref(false)
|
||||||
|
const noteInput = ref('')
|
||||||
|
|
||||||
|
const openNoteDialog = () => {
|
||||||
|
noteInput.value = props.npc.note || ''
|
||||||
|
noteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNote = () => {
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === props.npc.id)
|
||||||
|
if (npc) {
|
||||||
|
npc.note = noteInput.value.trim() || undefined
|
||||||
|
}
|
||||||
|
noteDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 好感度值
|
||||||
|
const reputation = computed(() => props.relation?.reputation || 0)
|
||||||
|
|
||||||
|
// 关系状态
|
||||||
|
const status = computed(() => props.relation?.status || RelationStatus.Neutral)
|
||||||
|
|
||||||
|
// 好感度颜色
|
||||||
|
const reputationColor = computed(() => {
|
||||||
|
if (reputation.value >= 20) return 'text-green-600 dark:text-green-400'
|
||||||
|
if (reputation.value <= -20) return 'text-red-600 dark:text-red-400'
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 最近的外交事件
|
||||||
|
const recentEvent = computed(() => {
|
||||||
|
if (!props.relation?.history || props.relation.history.length === 0) return null
|
||||||
|
return props.relation.history[props.relation.history.length - 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取盟友名称
|
||||||
|
const getAllyName = (allyId: string) => {
|
||||||
|
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||||
|
if (!ally) return allyId.substring(0, 8)
|
||||||
|
return ally.note ? `${ally.name}(${ally.note})` : ally.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取盟友边框样式
|
||||||
|
const getAllyBorderClass = (allyId: string) => {
|
||||||
|
const ally = npcStore.npcs.find(n => n.id === allyId)
|
||||||
|
if (!ally) return ''
|
||||||
|
|
||||||
|
const allyRelation = ally.relations?.[gameStore.player.id]
|
||||||
|
if (!allyRelation) return ''
|
||||||
|
|
||||||
|
switch (allyRelation.status) {
|
||||||
|
case RelationStatus.Friendly:
|
||||||
|
return 'border-green-500 dark:border-green-400'
|
||||||
|
case RelationStatus.Hostile:
|
||||||
|
return 'border-red-500 dark:border-red-400'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取事件图标
|
||||||
|
const getEventIcon = (eventType: string) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case DiplomaticEventType.GiftResources:
|
||||||
|
return Gift
|
||||||
|
case DiplomaticEventType.Attack:
|
||||||
|
case DiplomaticEventType.AllyAttacked:
|
||||||
|
return Sword
|
||||||
|
case DiplomaticEventType.Spy:
|
||||||
|
return Eye
|
||||||
|
case DiplomaticEventType.StealDebris:
|
||||||
|
return Trash2
|
||||||
|
default:
|
||||||
|
return Gift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取事件文本
|
||||||
|
const getEventText = (eventType: string) => {
|
||||||
|
switch (eventType) {
|
||||||
|
case DiplomaticEventType.GiftResources:
|
||||||
|
return t('diplomacy.events.gift')
|
||||||
|
case DiplomaticEventType.Attack:
|
||||||
|
return t('diplomacy.events.attack')
|
||||||
|
case DiplomaticEventType.AllyAttacked:
|
||||||
|
return t('diplomacy.events.allyAttacked')
|
||||||
|
case DiplomaticEventType.Spy:
|
||||||
|
return t('diplomacy.events.spy')
|
||||||
|
case DiplomaticEventType.StealDebris:
|
||||||
|
return t('diplomacy.events.stealDebris')
|
||||||
|
default:
|
||||||
|
return eventType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赠送资源
|
||||||
|
const handleGiftResources = () => {
|
||||||
|
if (props.npc.planets.length > 0) {
|
||||||
|
const targetPlanet = props.npc.planets[0]
|
||||||
|
if (!targetPlanet) return
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
path: '/fleet',
|
||||||
|
query: {
|
||||||
|
galaxy: targetPlanet.position.galaxy,
|
||||||
|
system: targetPlanet.position.system,
|
||||||
|
position: targetPlanet.position.position,
|
||||||
|
gift: '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看星球
|
||||||
|
const handleViewPlanets = () => {
|
||||||
|
if (props.npc.planets.length > 0) {
|
||||||
|
const targetPlanet = props.npc.planets[0]
|
||||||
|
if (!targetPlanet) return
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
path: '/galaxy',
|
||||||
|
query: {
|
||||||
|
galaxy: targetPlanet.position.galaxy,
|
||||||
|
system: targetPlanet.position.system,
|
||||||
|
highlightNpc: props.npc.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到盟友卡片
|
||||||
|
const scrollToAlly = (allyId: string) => {
|
||||||
|
const event = new CustomEvent('scrollToNpc', { detail: { npcId: allyId }, bubbles: true })
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
90
src/components/PrivacyDialog.vue
Normal file
90
src/components/PrivacyDialog.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model:open="open">
|
||||||
|
<DialogContent class="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ t('privacy.title') }}</DialogTitle>
|
||||||
|
<DialogDescription class="sr-only">{{ t('privacy.title') }}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="flex-1 overflow-y-auto pr-2 space-y-4 text-sm">
|
||||||
|
<!-- 简介 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.introduction.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground">{{ t('privacy.sections.introduction.content') }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 数据收集 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataCollection.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground mb-1">{{ t('privacy.sections.dataCollection.content') }}</p>
|
||||||
|
<ul class="list-disc list-inside text-muted-foreground ml-2 space-y-0.5">
|
||||||
|
<li>{{ t('privacy.sections.dataCollection.items.gameProgress') }}</li>
|
||||||
|
<li>{{ t('privacy.sections.dataCollection.items.settings') }}</li>
|
||||||
|
<li>{{ t('privacy.sections.dataCollection.items.language') }}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 数据存储 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataStorage.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground">{{ t('privacy.sections.dataStorage.content') }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 无服务器通信 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.noServer.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground">{{ t('privacy.sections.noServer.content') }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 第三方服务 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.thirdParty.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground">{{ t('privacy.sections.thirdParty.content') }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 数据控制 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.dataControl.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground mb-1">{{ t('privacy.sections.dataControl.content') }}</p>
|
||||||
|
<ul class="list-disc list-inside text-muted-foreground ml-2 space-y-0.5">
|
||||||
|
<li>{{ t('privacy.sections.dataControl.items.export') }}</li>
|
||||||
|
<li>{{ t('privacy.sections.dataControl.items.import') }}</li>
|
||||||
|
<li>{{ t('privacy.sections.dataControl.items.delete') }}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 联系我们 -->
|
||||||
|
<section>
|
||||||
|
<h3 class="font-semibold mb-1">{{ t('privacy.sections.contact.title') }}</h3>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
{{ t('privacy.sections.contact.content') }}
|
||||||
|
<a
|
||||||
|
:href="`https://github.com/${pkg.author.name}/${pkg.name}/issues`"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
GitHub Issues
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<DialogFooter class="mt-4">
|
||||||
|
<Button variant="outline" @click="open = false">
|
||||||
|
{{ t('common.close') }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import pkg from '../../package.json'
|
||||||
|
|
||||||
|
// 双向绑定 open 状态
|
||||||
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
200
src/components/QueueNotifications.vue
Normal file
200
src/components/QueueNotifications.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<Popover v-model:open="isOpen">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button data-tutorial="queue-button" variant="outline" size="icon" class="relative">
|
||||||
|
<ListOrdered class="h-4 w-4" />
|
||||||
|
<Badge
|
||||||
|
v-if="totalQueueCount > 0"
|
||||||
|
variant="default"
|
||||||
|
class="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
|
||||||
|
>
|
||||||
|
{{ totalQueueCount }}
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-96 p-0" align="end">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<h3 class="font-semibold">{{ t('queue.title') }} ({{ totalQueueCount }})</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
|
<TabsList class="w-full grid grid-cols-5 h-9 rounded-none border-b bg-transparent">
|
||||||
|
<TabsTrigger v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="text-xs px-1 data-[state=active]:bg-muted">
|
||||||
|
{{ t(`queue.tabs.${tab.value}`) }}
|
||||||
|
<Badge v-if="tab.items.length > 0" variant="secondary" class="ml-1 h-4 px-1 text-[10px]">
|
||||||
|
{{ tab.items.length }}
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<ScrollArea class="h-[420px]">
|
||||||
|
<TabsContent v-for="tab in tabConfig" :key="tab.value" :value="tab.value" class="mt-0">
|
||||||
|
<Empty v-if="tab.items.length === 0" class="border-0">
|
||||||
|
<EmptyContent>
|
||||||
|
<Inbox class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('queue.empty') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
<div v-else class="divide-y p-4 space-y-3">
|
||||||
|
<div v-for="item in tab.items" :key="item.id" class="space-y-1.5">
|
||||||
|
<div class="flex items-center justify-between text-xs sm:text-sm gap-2">
|
||||||
|
<div class="flex items-center gap-1.5 sm:gap-2 min-w-0 flex-1">
|
||||||
|
<div class="h-2 w-2 rounded-full animate-pulse flex-shrink-0" :class="getStatusDotClass(item)" />
|
||||||
|
<span class="font-medium truncate">{{ getItemName(item) }}</span>
|
||||||
|
<span class="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
{{
|
||||||
|
item.type === 'ship' || item.type === 'defense'
|
||||||
|
? `→ ${t('queue.quantity')} ${item.quantity}`
|
||||||
|
: item.type === 'demolish'
|
||||||
|
? `→ ${t('queue.demolishing')}`
|
||||||
|
: `→ ${t('queue.level')} ${item.targetLevel}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
||||||
|
<span class="text-muted-foreground text-[10px] sm:text-xs whitespace-nowrap">
|
||||||
|
{{ formatTime(getRemainingTime(item)) }}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-5 sm:h-6 px-1.5 sm:px-2 text-[10px] sm:text-xs"
|
||||||
|
@click.stop="handleCancel(item)"
|
||||||
|
>
|
||||||
|
{{ t('queue.cancel') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Progress :model-value="getQueueProgress(item)" class="h-1.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</ScrollArea>
|
||||||
|
</Tabs>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onUnmounted, watch } from 'vue'
|
||||||
|
import { ListOrdered, Inbox } from 'lucide-vue-next'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { formatTime } from '@/utils/format'
|
||||||
|
import type { BuildQueueItem, BuildingType, ShipType, DefenseType, TechnologyType } from '@/types/game'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { BUILDINGS, SHIPS, DEFENSES, TECHNOLOGIES } = useGameConfig()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 响应式时间戳,用于驱动时间和进度的动态更新
|
||||||
|
const currentTime = ref(Date.now())
|
||||||
|
let timerInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
// 当弹窗打开时启动计时器,关闭时停止
|
||||||
|
watch(isOpen, open => {
|
||||||
|
if (open) {
|
||||||
|
// 启动每秒更新的计时器
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
currentTime.value = Date.now()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// 停止计时器
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
timerInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理计时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
timerInterval = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前星球的建造队列
|
||||||
|
const buildQueue = computed(() => {
|
||||||
|
return gameStore.currentPlanet?.buildQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取研究队列
|
||||||
|
const researchQueue = computed(() => {
|
||||||
|
return gameStore.player.researchQueue || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 总队列数量
|
||||||
|
const totalQueueCount = computed(() => {
|
||||||
|
return buildQueue.value.length + researchQueue.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标签页配置(用于循环渲染)
|
||||||
|
const tabConfig = computed(() => [
|
||||||
|
{ value: 'all', items: [...buildQueue.value, ...researchQueue.value] },
|
||||||
|
{ value: 'buildings', items: buildQueue.value.filter(item => item.type === 'building' || item.type === 'demolish') },
|
||||||
|
{ value: 'research', items: researchQueue.value },
|
||||||
|
{ value: 'ships', items: buildQueue.value.filter(item => item.type === 'ship') },
|
||||||
|
{ value: 'defense', items: buildQueue.value.filter(item => item.type === 'defense') }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 获取队列项名称
|
||||||
|
const getItemName = (item: BuildQueueItem): string => {
|
||||||
|
if (item.type === 'building' || item.type === 'demolish') {
|
||||||
|
return BUILDINGS.value[item.itemType as BuildingType].name
|
||||||
|
} else if (item.type === 'ship') {
|
||||||
|
return SHIPS.value[item.itemType as ShipType].name
|
||||||
|
} else if (item.type === 'defense') {
|
||||||
|
return DEFENSES.value[item.itemType as DefenseType].name
|
||||||
|
} else if (item.type === 'technology') {
|
||||||
|
return TECHNOLOGIES.value[item.itemType as TechnologyType].name
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剩余时间(使用响应式 currentTime 确保动态更新)
|
||||||
|
const getRemainingTime = (item: BuildQueueItem): number => {
|
||||||
|
return Math.max(0, Math.floor((item.endTime - currentTime.value) / 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取队列进度(使用响应式 currentTime 确保动态更新)
|
||||||
|
const getQueueProgress = (item: BuildQueueItem): number => {
|
||||||
|
const elapsed = currentTime.value - item.startTime
|
||||||
|
const total = item.endTime - item.startTime
|
||||||
|
return Math.min(100, (elapsed / total) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的取消处理
|
||||||
|
const handleCancel = (item: BuildQueueItem) => {
|
||||||
|
let eventName: string
|
||||||
|
if (item.type === 'building' || item.type === 'ship' || item.type === 'defense' || item.type === 'demolish') {
|
||||||
|
eventName = 'cancel-build'
|
||||||
|
} else if (item.type === 'technology') {
|
||||||
|
eventName = 'cancel-research'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new CustomEvent(eventName, { detail: item.id })
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态指示点颜色
|
||||||
|
const getStatusDotClass = (item: BuildQueueItem): string => {
|
||||||
|
if (item.type === 'demolish') return 'bg-destructive'
|
||||||
|
if (item.type === 'technology') return 'bg-blue-500'
|
||||||
|
return 'bg-green-500'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto min-h-0 mt-4 pr-2">
|
<div class="flex-1 overflow-y-auto min-h-0 mt-4 pr-2">
|
||||||
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown"></div>
|
<div class="prose prose-sm dark:prose-invert max-w-none" v-html="renderedMarkdown" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter class="flex gap-2 flex-shrink-0 mt-4">
|
<DialogFooter class="flex gap-2 flex-shrink-0 mt-4">
|
||||||
|
|||||||
159
src/components/ui/bg-stars/StarsBackground.vue
Normal file
159
src/components/ui/bg-stars/StarsBackground.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn('relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]', props.class)"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
>
|
||||||
|
<motion.div :style="{ x: springX, y: springY }">
|
||||||
|
<!-- Star Layer 1 -->
|
||||||
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer1Transition">
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
boxShadow: boxShadow1
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
boxShadow: boxShadow1
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Star Layer 2 -->
|
||||||
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer2Transition">
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '2px',
|
||||||
|
height: '2px',
|
||||||
|
boxShadow: boxShadow2
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '2px',
|
||||||
|
height: '2px',
|
||||||
|
boxShadow: boxShadow2
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Star Layer 3 -->
|
||||||
|
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer3Transition">
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: '3px',
|
||||||
|
height: '3px',
|
||||||
|
boxShadow: boxShadow3
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bg-transparent rounded-full top-[2000px]"
|
||||||
|
:style="{
|
||||||
|
width: '3px',
|
||||||
|
height: '3px',
|
||||||
|
boxShadow: boxShadow3
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<!-- Slot for child content -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SpringOptions, Transition } from 'motion-v'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { motion, useMotionValue, useSpring } from 'motion-v'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
interface StarsBackgroundProps {
|
||||||
|
factor?: number
|
||||||
|
speed?: number
|
||||||
|
transition?: SpringOptions
|
||||||
|
starColor?: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
|
||||||
|
factor: 0.05,
|
||||||
|
speed: 50,
|
||||||
|
transition: () => ({ stiffness: 50, damping: 20 }),
|
||||||
|
starColor: '#fff'
|
||||||
|
})
|
||||||
|
|
||||||
|
// For slot content
|
||||||
|
defineSlots()
|
||||||
|
|
||||||
|
function generateStars(count: number, starColor: string) {
|
||||||
|
const shadows: string[] = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = Math.floor(Math.random() * 4000) - 2000
|
||||||
|
const y = Math.floor(Math.random() * 4000) - 2000
|
||||||
|
shadows.push(`${x}px ${y}px ${starColor}`)
|
||||||
|
}
|
||||||
|
return shadows.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetX = useMotionValue(1)
|
||||||
|
const offsetY = useMotionValue(1)
|
||||||
|
|
||||||
|
const springX = useSpring(offsetX, props.transition)
|
||||||
|
const springY = useSpring(offsetY, props.transition)
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
const centerX = window.innerWidth / 2
|
||||||
|
const centerY = window.innerHeight / 2
|
||||||
|
const newOffsetX = -(e.clientX - centerX) * props.factor
|
||||||
|
const newOffsetY = -(e.clientY - centerY) * props.factor
|
||||||
|
offsetX.set(newOffsetX)
|
||||||
|
offsetY.set(newOffsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxShadow1 = ref('')
|
||||||
|
const boxShadow2 = ref('')
|
||||||
|
const boxShadow3 = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
boxShadow1.value = generateStars(1000, props.starColor)
|
||||||
|
boxShadow2.value = generateStars(400, props.starColor)
|
||||||
|
boxShadow3.value = generateStars(200, props.starColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for starColor changes
|
||||||
|
watch(
|
||||||
|
() => props.starColor,
|
||||||
|
newColor => {
|
||||||
|
boxShadow1.value = generateStars(1000, newColor)
|
||||||
|
boxShadow2.value = generateStars(400, newColor)
|
||||||
|
boxShadow3.value = generateStars(200, newColor)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const starLayer1Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed,
|
||||||
|
ease: 'linear' as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
const starLayer2Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed * 2,
|
||||||
|
ease: 'linear' as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
const starLayer3Transition = computed<Transition>(() => ({
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: props.speed * 3,
|
||||||
|
ease: 'linear' as const
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
1
src/components/ui/bg-stars/index.ts
Normal file
1
src/components/ui/bg-stars/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as StarsBackground } from './StarsBackground.vue'
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[60] bg-black/80',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
|
||||||
containerClass
|
containerClass
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 可滚动的内容区域 -->
|
<!-- 可滚动的内容区域 -->
|
||||||
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4">
|
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4 max-h-[60vh]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
91
src/components/ui/pagination/FixedPagination.vue
Normal file
91
src/components/ui/pagination/FixedPagination.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="totalPages > 1" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 上一页按钮 - 圆形胶囊 -->
|
||||||
|
<button
|
||||||
|
v-if="currentPage > 1"
|
||||||
|
@click="emit('update:page', currentPage - 1)"
|
||||||
|
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 页码 - 椭圆形胶囊 -->
|
||||||
|
<div
|
||||||
|
class="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border rounded-full py-2 px-3 shadow-lg flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="pageNum in pageNumbers"
|
||||||
|
:key="pageNum"
|
||||||
|
@click="emit('update:page', pageNum)"
|
||||||
|
class="h-8 min-w-8 px-2 rounded-full text-sm font-medium transition-colors"
|
||||||
|
:class="pageNum === currentPage ? 'bg-primary text-primary-foreground' : 'hover:bg-accent'"
|
||||||
|
>
|
||||||
|
{{ pageNum }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下一页按钮 - 圆形胶囊 -->
|
||||||
|
<button
|
||||||
|
v-if="currentPage < totalPages"
|
||||||
|
@click="emit('update:page', currentPage + 1)"
|
||||||
|
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: number
|
||||||
|
totalPages: number
|
||||||
|
maxVisible?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
maxVisible: 3
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:page': [page: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const currentPage = computed(() => props.page)
|
||||||
|
|
||||||
|
// 生成页码列表 - 最多显示指定数量页码,不含省略号
|
||||||
|
const pageNumbers = computed(() => {
|
||||||
|
const pages: number[] = []
|
||||||
|
const { totalPages, maxVisible } = props
|
||||||
|
const current = currentPage.value
|
||||||
|
|
||||||
|
if (totalPages <= maxVisible) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let start = current - Math.floor(maxVisible / 2)
|
||||||
|
let end = current + Math.floor(maxVisible / 2)
|
||||||
|
|
||||||
|
// 边界调整
|
||||||
|
if (start < 1) {
|
||||||
|
start = 1
|
||||||
|
end = maxVisible
|
||||||
|
}
|
||||||
|
if (end > totalPages) {
|
||||||
|
end = totalPages
|
||||||
|
start = totalPages - maxVisible + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -6,3 +6,4 @@ export { default as PaginationItem } from './PaginationItem.vue'
|
|||||||
export { default as PaginationLast } from './PaginationLast.vue'
|
export { default as PaginationLast } from './PaginationLast.vue'
|
||||||
export { default as PaginationNext } from './PaginationNext.vue'
|
export { default as PaginationNext } from './PaginationNext.vue'
|
||||||
export { default as PaginationPrevious } from './PaginationPrevious.vue'
|
export { default as PaginationPrevious } from './PaginationPrevious.vue'
|
||||||
|
export { default as FixedPagination } from './FixedPagination.vue'
|
||||||
|
|||||||
238
src/components/ui/particles-bg/ParticlesBg.vue
Normal file
238
src/components/ui/particles-bg/ParticlesBg.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="canvasContainerRef" :class="$props.class" aria-hidden="true">
|
||||||
|
<canvas ref="canvasRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMouse, useDevicePixelRatio } from '@vueuse/core'
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'
|
||||||
|
|
||||||
|
type Circle = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
translateX: number
|
||||||
|
translateY: number
|
||||||
|
size: number
|
||||||
|
alpha: number
|
||||||
|
targetAlpha: number
|
||||||
|
dx: number
|
||||||
|
dy: number
|
||||||
|
magnetism: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
color?: string
|
||||||
|
quantity?: number
|
||||||
|
staticity?: number
|
||||||
|
ease?: number
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
color: '#FFF',
|
||||||
|
quantity: 100,
|
||||||
|
staticity: 50,
|
||||||
|
ease: 50,
|
||||||
|
class: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const canvasContainerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const context = ref<CanvasRenderingContext2D | null>(null)
|
||||||
|
const circles = ref<Circle[]>([])
|
||||||
|
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||||
|
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
|
||||||
|
const { x: mouseX, y: mouseY } = useMouse()
|
||||||
|
const { pixelRatio } = useDevicePixelRatio()
|
||||||
|
|
||||||
|
const color = computed(() => {
|
||||||
|
// Remove the leading '#' if it's present
|
||||||
|
let hex = props.color.replace(/^#/, '')
|
||||||
|
|
||||||
|
// If the hex code is 3 characters, expand it to 6 characters
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split('')
|
||||||
|
.map(char => char + char)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the r, g, b values from the hex string
|
||||||
|
const bigint = parseInt(hex, 16)
|
||||||
|
const r = (bigint >> 16) & 255 // Extract the red component
|
||||||
|
const g = (bigint >> 8) & 255 // Extract the green component
|
||||||
|
const b = bigint & 255 // Extract the blue component
|
||||||
|
|
||||||
|
// Return the RGB values as a string separated by spaces
|
||||||
|
return `${r} ${g} ${b}`
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
context.value = canvasRef.value.getContext('2d')
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas()
|
||||||
|
animate()
|
||||||
|
window.addEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', initCanvas)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([mouseX, mouseY], () => {
|
||||||
|
onMouseMove()
|
||||||
|
})
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
resizeCanvas()
|
||||||
|
drawParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove() {
|
||||||
|
if (canvasRef.value) {
|
||||||
|
const rect = canvasRef.value.getBoundingClientRect()
|
||||||
|
const { w, h } = canvasSize
|
||||||
|
const x = mouseX.value - rect.left - w / 2
|
||||||
|
const y = mouseY.value - rect.top - h / 2
|
||||||
|
|
||||||
|
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
|
||||||
|
if (inside) {
|
||||||
|
mouse.x = x
|
||||||
|
mouse.y = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (canvasContainerRef.value && canvasRef.value && context.value) {
|
||||||
|
circles.value.length = 0
|
||||||
|
canvasSize.w = canvasContainerRef.value.offsetWidth
|
||||||
|
canvasSize.h = canvasContainerRef.value.offsetHeight
|
||||||
|
canvasRef.value.width = canvasSize.w * pixelRatio.value
|
||||||
|
canvasRef.value.height = canvasSize.h * pixelRatio.value
|
||||||
|
canvasRef.value.style.width = canvasSize.w + 'px'
|
||||||
|
canvasRef.value.style.height = canvasSize.h + 'px'
|
||||||
|
context.value.scale(pixelRatio.value, pixelRatio.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function circleParams(): Circle {
|
||||||
|
const x = Math.floor(Math.random() * canvasSize.w)
|
||||||
|
const y = Math.floor(Math.random() * canvasSize.h)
|
||||||
|
const translateX = 0
|
||||||
|
const translateY = 0
|
||||||
|
const size = Math.floor(Math.random() * 2) + 1
|
||||||
|
const alpha = 0
|
||||||
|
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
|
||||||
|
const dx = (Math.random() - 0.5) * 0.2
|
||||||
|
const dy = (Math.random() - 0.5) * 0.2
|
||||||
|
const magnetism = 0.1 + Math.random() * 4
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
translateX,
|
||||||
|
translateY,
|
||||||
|
size,
|
||||||
|
alpha,
|
||||||
|
targetAlpha,
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
magnetism
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCircle(circle: Circle, update = false) {
|
||||||
|
if (context.value) {
|
||||||
|
const { x, y, translateX, translateY, size, alpha } = circle
|
||||||
|
context.value.translate(translateX, translateY)
|
||||||
|
context.value.beginPath()
|
||||||
|
context.value.arc(x, y, size, 0, 2 * Math.PI)
|
||||||
|
context.value.fillStyle = `rgba(${color.value.split(' ').join(', ')}, ${alpha})`
|
||||||
|
context.value.fill()
|
||||||
|
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0)
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
circles.value.push(circle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearContext() {
|
||||||
|
if (context.value) {
|
||||||
|
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawParticles() {
|
||||||
|
clearContext()
|
||||||
|
const particleCount = props.quantity
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const circle = circleParams()
|
||||||
|
drawCircle(circle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapValue(value: number, start1: number, end1: number, start2: number, end2: number): number {
|
||||||
|
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
|
||||||
|
return remapped > 0 ? remapped : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
clearContext()
|
||||||
|
circles.value.forEach((circle, i) => {
|
||||||
|
// Handle the alpha value
|
||||||
|
const edge = [
|
||||||
|
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||||
|
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||||
|
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||||
|
canvasSize.h - circle.y - circle.translateY - circle.size // distance from bottom edge
|
||||||
|
]
|
||||||
|
|
||||||
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
|
||||||
|
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
|
||||||
|
|
||||||
|
if (remapClosestEdge > 1) {
|
||||||
|
circle.alpha += 0.02
|
||||||
|
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
|
||||||
|
} else {
|
||||||
|
circle.alpha = circle.targetAlpha * remapClosestEdge
|
||||||
|
}
|
||||||
|
|
||||||
|
circle.x += circle.dx
|
||||||
|
circle.y += circle.dy
|
||||||
|
circle.translateX += (mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease
|
||||||
|
circle.translateY += (mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease
|
||||||
|
|
||||||
|
// circle gets out of the canvas
|
||||||
|
if (
|
||||||
|
circle.x < -circle.size ||
|
||||||
|
circle.x > canvasSize.w + circle.size ||
|
||||||
|
circle.y < -circle.size ||
|
||||||
|
circle.y > canvasSize.h + circle.size
|
||||||
|
) {
|
||||||
|
// remove the circle from the array
|
||||||
|
circles.value.splice(i, 1)
|
||||||
|
// create a new circle
|
||||||
|
const newCircle = circleParams()
|
||||||
|
drawCircle(newCircle)
|
||||||
|
// update the circle position
|
||||||
|
} else {
|
||||||
|
drawCircle(
|
||||||
|
{
|
||||||
|
...circle,
|
||||||
|
x: circle.x,
|
||||||
|
y: circle.y,
|
||||||
|
translateX: circle.translateX,
|
||||||
|
translateY: circle.translateY,
|
||||||
|
alpha: circle.alpha
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
1
src/components/ui/particles-bg/index.ts
Normal file
1
src/components/ui/particles-bg/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as ParticlesBg } from './ParticlesBg.vue'
|
||||||
@@ -1,20 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { SeparatorProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { Separator } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<
|
|
||||||
SeparatorProps & { class?: HTMLAttributes["class"] }
|
|
||||||
>(), {
|
|
||||||
orientation: "horizontal",
|
|
||||||
decorative: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Separator
|
<Separator
|
||||||
data-slot="separator"
|
data-slot="separator"
|
||||||
@@ -22,8 +5,23 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||||
props.class,
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SeparatorProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { Separator } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
decorative: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Separator } from "./Separator.vue"
|
export { default as Separator } from './Separator.vue'
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<DialogRoot v-slot="slotProps" data-slot="sheet" v-bind="forwarded">
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||||
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
|
||||||
const props = defineProps<DialogRootProps>()
|
const props = defineProps<DialogRootProps>()
|
||||||
const emits = defineEmits<DialogRootEmits>()
|
const emits = defineEmits<DialogRootEmits>()
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits)
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<DialogRoot
|
|
||||||
v-slot="slotProps"
|
|
||||||
data-slot="sheet"
|
|
||||||
v-bind="forwarded"
|
|
||||||
>
|
|
||||||
<slot v-bind="slotProps" />
|
|
||||||
</DialogRoot>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogCloseProps } from "reka-ui"
|
|
||||||
import { DialogClose } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<DialogCloseProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogClose
|
<DialogClose data-slot="sheet-close" v-bind="props">
|
||||||
data-slot="sheet-close"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from 'reka-ui'
|
||||||
|
import { DialogClose } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,21 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { X } from "lucide-vue-next"
|
|
||||||
import {
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import SheetOverlay from "./SheetOverlay.vue"
|
|
||||||
|
|
||||||
interface SheetContentProps extends DialogContentProps {
|
|
||||||
class?: HTMLAttributes["class"]
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
}
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
inheritAttrs: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
|
||||||
side: "right",
|
|
||||||
})
|
|
||||||
const emits = defineEmits<DialogContentEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class", "side")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<DialogContent
|
<DialogContent
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
:class="cn(
|
:class="
|
||||||
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
side === 'right'
|
side === 'right' &&
|
||||||
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||||
side === 'left'
|
side === 'left' &&
|
||||||
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||||
side === 'top'
|
side === 'top' && 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||||
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
side === 'bottom' &&
|
||||||
side === 'bottom'
|
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||||
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
props.class
|
||||||
props.class)"
|
)
|
||||||
|
"
|
||||||
v-bind="{ ...$attrs, ...forwarded }"
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -60,3 +29,31 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import SheetOverlay from './SheetOverlay.vue'
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||||
|
side: 'right'
|
||||||
|
})
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'side')
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogDescriptionProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogDescription } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogDescription
|
<DialogDescription data-slot="sheet-description" :class="cn('text-muted-foreground text-sm', props.class)" v-bind="delegatedProps">
|
||||||
data-slot="sheet-description"
|
|
||||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogDescription } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div data-slot="sheet-footer" :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
|
||||||
data-slot="sheet-footer"
|
|
||||||
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div data-slot="sheet-header" :class="cn('flex flex-col gap-1.5 p-4', props.class)">
|
||||||
data-slot="sheet-header"
|
|
||||||
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogOverlayProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogOverlay } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogOverlay
|
<DialogOverlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
:class="
|
||||||
|
cn(
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</DialogOverlay>
|
</DialogOverlay>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogOverlayProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogOverlay } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogTitleProps } from "reka-ui"
|
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import { DialogTitle } from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogTitle
|
<DialogTitle data-slot="sheet-title" :class="cn('text-foreground font-semibold', props.class)" v-bind="delegatedProps">
|
||||||
data-slot="sheet-title"
|
|
||||||
:class="cn('text-foreground font-semibold', props.class)"
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { reactiveOmit } from '@vueuse/core'
|
||||||
|
import { DialogTitle } from 'reka-ui'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class')
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { DialogTriggerProps } from "reka-ui"
|
|
||||||
import { DialogTrigger } from "reka-ui"
|
|
||||||
|
|
||||||
const props = defineProps<DialogTriggerProps>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogTrigger
|
<DialogTrigger data-slot="sheet-trigger" v-bind="props">
|
||||||
data-slot="sheet-trigger"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from 'reka-ui'
|
||||||
|
import { DialogTrigger } from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export { default as Sheet } from "./Sheet.vue"
|
export { default as Sheet } from './Sheet.vue'
|
||||||
export { default as SheetClose } from "./SheetClose.vue"
|
export { default as SheetClose } from './SheetClose.vue'
|
||||||
export { default as SheetContent } from "./SheetContent.vue"
|
export { default as SheetContent } from './SheetContent.vue'
|
||||||
export { default as SheetDescription } from "./SheetDescription.vue"
|
export { default as SheetDescription } from './SheetDescription.vue'
|
||||||
export { default as SheetFooter } from "./SheetFooter.vue"
|
export { default as SheetFooter } from './SheetFooter.vue'
|
||||||
export { default as SheetHeader } from "./SheetHeader.vue"
|
export { default as SheetHeader } from './SheetHeader.vue'
|
||||||
export { default as SheetTitle } from "./SheetTitle.vue"
|
export { default as SheetTitle } from './SheetTitle.vue'
|
||||||
export { default as SheetTrigger } from "./SheetTrigger.vue"
|
export { default as SheetTrigger } from './SheetTrigger.vue'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
|
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="handleOpenMobileChange">
|
||||||
<SheetContent
|
<SheetContent
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar"
|
data-slot="sidebar"
|
||||||
@@ -79,12 +79,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SidebarProps } from '.'
|
import type { SidebarProps } from '.'
|
||||||
|
import { watch } from 'vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||||
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
||||||
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
|
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
|
||||||
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
|
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
|
||||||
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false
|
inheritAttrs: false
|
||||||
@@ -96,5 +98,21 @@
|
|||||||
collapsible: 'offcanvas'
|
collapsible: 'offcanvas'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||||
|
|
||||||
|
// 处理移动端侧边栏打开/关闭
|
||||||
|
const handleOpenMobileChange = (open: boolean) => {
|
||||||
|
setOpenMobile(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化,在移动端关闭侧边栏
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value.path,
|
||||||
|
() => {
|
||||||
|
if (isMobile.value && openMobile.value) {
|
||||||
|
setOpenMobile(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,18 +16,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes, Ref } from 'vue'
|
import type { HTMLAttributes, Ref } from 'vue'
|
||||||
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
|
import { useMediaQuery, useVModel } from '@vueuse/core'
|
||||||
import { TooltipProvider } from 'reka-ui'
|
import { TooltipProvider } from 'reka-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import { provideSidebarContext, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
|
||||||
provideSidebarContext,
|
|
||||||
SIDEBAR_COOKIE_MAX_AGE,
|
|
||||||
SIDEBAR_COOKIE_NAME,
|
|
||||||
SIDEBAR_KEYBOARD_SHORTCUT,
|
|
||||||
SIDEBAR_WIDTH,
|
|
||||||
SIDEBAR_WIDTH_ICON
|
|
||||||
} from './utils'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -36,7 +29,7 @@
|
|||||||
class?: HTMLAttributes['class']
|
class?: HTMLAttributes['class']
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
defaultOpen: true,
|
||||||
open: undefined
|
open: undefined
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -54,30 +47,17 @@
|
|||||||
}) as Ref<boolean>
|
}) as Ref<boolean>
|
||||||
|
|
||||||
const setOpen = (value: boolean) => {
|
const setOpen = (value: boolean) => {
|
||||||
open.value = value // emits('update:open', value)
|
open.value = value
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setOpenMobile = (value: boolean) => {
|
const setOpenMobile = (value: boolean) => {
|
||||||
openMobile.value = value
|
openMobile.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEventListener('keydown', (event: KeyboardEvent) => {
|
|
||||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
|
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
|
||||||
|
|
||||||
provideSidebarContext({
|
provideSidebarContext({
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
import { createContext } from 'reka-ui'
|
import { createContext } from 'reka-ui'
|
||||||
|
|
||||||
export const SIDEBAR_COOKIE_NAME = 'sidebar_state'
|
|
||||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
||||||
export const SIDEBAR_WIDTH = '16rem'
|
export const SIDEBAR_WIDTH = '16rem'
|
||||||
export const SIDEBAR_WIDTH_MOBILE = '18rem'
|
export const SIDEBAR_WIDTH_MOBILE = '18rem'
|
||||||
export const SIDEBAR_WIDTH_ICON = '3rem'
|
export const SIDEBAR_WIDTH_ICON = '3rem'
|
||||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
|
|
||||||
|
|
||||||
export const [useSidebar, provideSidebarContext] = createContext<{
|
export const [useSidebar, provideSidebarContext] = createContext<{
|
||||||
state: ComputedRef<'expanded' | 'collapsed'>
|
state: ComputedRef<'expanded' | 'collapsed'>
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div data-slot="skeleton" :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { HTMLAttributes } from "vue"
|
import type { HTMLAttributes } from 'vue'
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface SkeletonProps {
|
interface SkeletonProps {
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes['class']
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<SkeletonProps>()
|
const props = defineProps<SkeletonProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Skeleton } from "./Skeleton.vue"
|
export { default as Skeleton } from './Skeleton.vue'
|
||||||
|
|||||||
@@ -41,3 +41,9 @@
|
|||||||
|
|
||||||
const props = defineProps<ToasterProps>()
|
const props = defineProps<ToasterProps>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dark [data-sonner-toast][data-styled='true'] [data-description] {
|
||||||
|
color: oklch(0.91 0 0 / 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
48
src/components/ui/switch/Switch.vue
Normal file
48
src/components/ui/switch/Switch.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="checked"
|
||||||
|
:data-state="checked ? 'checked' : 'unchecked'"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
checked ? 'bg-primary' : 'bg-input',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:data-state="checked ? 'checked' : 'unchecked'"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform',
|
||||||
|
checked ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { type HTMLAttributes } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
checked?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:checked', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (props.disabled) return
|
||||||
|
emit('update:checked', !props.checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
2
src/components/ui/switch/index.ts
Normal file
2
src/components/ui/switch/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Switch } from './Switch.vue'
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export const useGameConfig = () => {
|
|||||||
[BuildingType.RoboticsFactory]: 'roboticsFactory',
|
[BuildingType.RoboticsFactory]: 'roboticsFactory',
|
||||||
[BuildingType.NaniteFactory]: 'naniteFactory',
|
[BuildingType.NaniteFactory]: 'naniteFactory',
|
||||||
[BuildingType.Shipyard]: 'shipyard',
|
[BuildingType.Shipyard]: 'shipyard',
|
||||||
|
[BuildingType.Hangar]: 'hangar',
|
||||||
[BuildingType.ResearchLab]: 'researchLab',
|
[BuildingType.ResearchLab]: 'researchLab',
|
||||||
[BuildingType.MetalStorage]: 'metalStorage',
|
[BuildingType.MetalStorage]: 'metalStorage',
|
||||||
[BuildingType.CrystalStorage]: 'crystalStorage',
|
[BuildingType.CrystalStorage]: 'crystalStorage',
|
||||||
|
|||||||
239
src/composables/useHints.ts
Normal file
239
src/composables/useHints.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* 弱引导系统 - 非阻塞式、可关闭的页面提示
|
||||||
|
* 用温和的引导替代强制性教程
|
||||||
|
*/
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
|
// 提示定义 - 用户首次访问页面时显示一次
|
||||||
|
export interface Hint {
|
||||||
|
id: string
|
||||||
|
route: string // 显示此提示的路由路径
|
||||||
|
titleKey: string // 标题的 i18n 键
|
||||||
|
messageKey: string // 消息的 i18n 键
|
||||||
|
icon?: string // 可选的图标名称
|
||||||
|
delay?: number // 显示前的延迟毫秒数(默认 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有可用的提示
|
||||||
|
const hints: Hint[] = [
|
||||||
|
{
|
||||||
|
id: 'overview_intro',
|
||||||
|
route: '/overview',
|
||||||
|
titleKey: 'hints.overview.title',
|
||||||
|
messageKey: 'hints.overview.message',
|
||||||
|
icon: 'home',
|
||||||
|
delay: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'buildings_intro',
|
||||||
|
route: '/buildings',
|
||||||
|
titleKey: 'hints.buildings.title',
|
||||||
|
messageKey: 'hints.buildings.message',
|
||||||
|
icon: 'building',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'research_intro',
|
||||||
|
route: '/research',
|
||||||
|
titleKey: 'hints.research.title',
|
||||||
|
messageKey: 'hints.research.message',
|
||||||
|
icon: 'flask',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shipyard_intro',
|
||||||
|
route: '/shipyard',
|
||||||
|
titleKey: 'hints.shipyard.title',
|
||||||
|
messageKey: 'hints.shipyard.message',
|
||||||
|
icon: 'rocket',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fleet_intro',
|
||||||
|
route: '/fleet',
|
||||||
|
titleKey: 'hints.fleet.title',
|
||||||
|
messageKey: 'hints.fleet.message',
|
||||||
|
icon: 'plane',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'galaxy_intro',
|
||||||
|
route: '/galaxy',
|
||||||
|
titleKey: 'hints.galaxy.title',
|
||||||
|
messageKey: 'hints.galaxy.message',
|
||||||
|
icon: 'globe',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'diplomacy_intro',
|
||||||
|
route: '/diplomacy',
|
||||||
|
titleKey: 'hints.diplomacy.title',
|
||||||
|
messageKey: 'hints.diplomacy.message',
|
||||||
|
icon: 'handshake',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'messages_intro',
|
||||||
|
route: '/messages',
|
||||||
|
titleKey: 'hints.messages.title',
|
||||||
|
messageKey: 'hints.messages.message',
|
||||||
|
icon: 'mail',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'defense_intro',
|
||||||
|
route: '/defense',
|
||||||
|
titleKey: 'hints.defense.title',
|
||||||
|
messageKey: 'hints.defense.message',
|
||||||
|
icon: 'shield',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'officers_intro',
|
||||||
|
route: '/officers',
|
||||||
|
titleKey: 'hints.officers.title',
|
||||||
|
messageKey: 'hints.officers.message',
|
||||||
|
icon: 'users',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'simulator_intro',
|
||||||
|
route: '/battle-simulator',
|
||||||
|
titleKey: 'hints.simulator.title',
|
||||||
|
messageKey: 'hints.simulator.message',
|
||||||
|
icon: 'swords',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings_intro',
|
||||||
|
route: '/settings',
|
||||||
|
titleKey: 'hints.settings.title',
|
||||||
|
messageKey: 'hints.settings.message',
|
||||||
|
icon: 'settings',
|
||||||
|
delay: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gm_intro',
|
||||||
|
route: '/gm',
|
||||||
|
titleKey: 'hints.gm.title',
|
||||||
|
messageKey: 'hints.gm.message',
|
||||||
|
icon: 'wand',
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 全局UI状态(不需要持久化)
|
||||||
|
const currentHint = ref<Hint | null>(null)
|
||||||
|
const isHintVisible = ref(false)
|
||||||
|
|
||||||
|
let hintTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
export function useHints() {
|
||||||
|
const router = useRouter()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
// 确保 dismissedHints 数组已初始化
|
||||||
|
if (!gameStore.player.dismissedHints) {
|
||||||
|
gameStore.player.dismissedHints = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 hintsEnabled 已初始化(默认为 true)
|
||||||
|
if (gameStore.player.hintsEnabled === undefined) {
|
||||||
|
gameStore.player.hintsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查提示是否已被关闭
|
||||||
|
const isHintDismissed = (hintId: string): boolean => {
|
||||||
|
return gameStore.player.dismissedHints?.includes(hintId) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前路由是否应该显示提示
|
||||||
|
const checkForHint = (routePath: string) => {
|
||||||
|
if (!gameStore.player.hintsEnabled) return
|
||||||
|
|
||||||
|
// 清除任何待显示的提示
|
||||||
|
if (hintTimeout) {
|
||||||
|
clearTimeout(hintTimeout)
|
||||||
|
hintTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航时隐藏当前提示
|
||||||
|
isHintVisible.value = false
|
||||||
|
currentHint.value = null
|
||||||
|
|
||||||
|
// 查找此路由对应的提示
|
||||||
|
const hint = hints.find(h => routePath.startsWith(h.route))
|
||||||
|
if (!hint) return
|
||||||
|
|
||||||
|
// 检查是否已经关闭过
|
||||||
|
if (isHintDismissed(hint.id)) return
|
||||||
|
|
||||||
|
// 延迟后显示提示
|
||||||
|
const delay = hint.delay ?? 500
|
||||||
|
hintTimeout = setTimeout(() => {
|
||||||
|
currentHint.value = hint
|
||||||
|
isHintVisible.value = true
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭当前提示(不再显示)
|
||||||
|
const dismissHint = (dontShowAgain = true) => {
|
||||||
|
if (currentHint.value && dontShowAgain) {
|
||||||
|
if (!gameStore.player.dismissedHints) {
|
||||||
|
gameStore.player.dismissedHints = []
|
||||||
|
}
|
||||||
|
if (!gameStore.player.dismissedHints.includes(currentHint.value.id)) {
|
||||||
|
gameStore.player.dismissedHints.push(currentHint.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isHintVisible.value = false
|
||||||
|
currentHint.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂时关闭提示(下次访问还会显示)
|
||||||
|
const closeHint = () => {
|
||||||
|
isHintVisible.value = false
|
||||||
|
currentHint.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置所有提示(重新显示)
|
||||||
|
const resetHints = () => {
|
||||||
|
gameStore.player.dismissedHints = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换提示开关
|
||||||
|
const setHintsEnabled = (enabled: boolean) => {
|
||||||
|
gameStore.player.hintsEnabled = enabled
|
||||||
|
if (!enabled) {
|
||||||
|
isHintVisible.value = false
|
||||||
|
currentHint.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value.path,
|
||||||
|
newPath => {
|
||||||
|
checkForHint(newPath)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
currentHint: computed(() => currentHint.value),
|
||||||
|
isHintVisible: computed(() => isHintVisible.value),
|
||||||
|
hintsEnabled: computed(() => gameStore.player.hintsEnabled ?? true),
|
||||||
|
dismissedCount: computed(() => gameStore.player.dismissedHints?.length ?? 0),
|
||||||
|
totalHints: computed(() => hints.length),
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
dismissHint,
|
||||||
|
closeHint,
|
||||||
|
resetHints,
|
||||||
|
setHintsEnabled,
|
||||||
|
checkForHint
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,6 +130,21 @@ export const BUILDINGS: Record<BuildingType, BuildingConfig> = {
|
|||||||
12: { [BuildingType.RoboticsFactory]: 8, [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 2 }
|
12: { [BuildingType.RoboticsFactory]: 8, [BuildingType.ResearchLab]: 8, [BuildingType.NaniteFactory]: 2 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[BuildingType.Hangar]: {
|
||||||
|
id: BuildingType.Hangar,
|
||||||
|
name: '机库',
|
||||||
|
description: '专门用于扩展舰队存储容量,支持星球专业化发展',
|
||||||
|
baseCost: { metal: 200, crystal: 100, deuterium: 50, darkMatter: 0, energy: 0 },
|
||||||
|
baseTime: 20,
|
||||||
|
costMultiplier: 1.8,
|
||||||
|
spaceUsage: 3,
|
||||||
|
fleetStorageBonus: 1500, // 每级增加1500舰队仓储,比船坞更高
|
||||||
|
requirements: { [BuildingType.RoboticsFactory]: 1 }, // 只需要1级机器人工厂
|
||||||
|
levelRequirements: {
|
||||||
|
10: { [BuildingType.RoboticsFactory]: 3 },
|
||||||
|
20: { [BuildingType.RoboticsFactory]: 5 }
|
||||||
|
}
|
||||||
|
},
|
||||||
[BuildingType.ResearchLab]: {
|
[BuildingType.ResearchLab]: {
|
||||||
id: BuildingType.ResearchLab,
|
id: BuildingType.ResearchLab,
|
||||||
name: '研究实验室',
|
name: '研究实验室',
|
||||||
@@ -429,12 +444,12 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
[TechnologyType.ComputerTechnology]: {
|
[TechnologyType.ComputerTechnology]: {
|
||||||
id: TechnologyType.ComputerTechnology,
|
id: TechnologyType.ComputerTechnology,
|
||||||
name: '计算机技术',
|
name: '计算机技术',
|
||||||
description: '增加研究队列数量,每级+1队列',
|
description: '增加研究队列和舰队任务槽位,每级+1队列+1槽位',
|
||||||
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
|
baseCost: { metal: 0, crystal: 400, deuterium: 600, darkMatter: 0, energy: 0 },
|
||||||
baseTime: 60,
|
baseTime: 60,
|
||||||
costMultiplier: 2,
|
costMultiplier: 2,
|
||||||
fleetStorageBonus: 500, // 每级全局增加500舰队仓储
|
fleetStorageBonus: 500, // 每级全局增加500舰队仓储
|
||||||
maxLevel: 10, // 最多10级(最多11个研究队列)
|
maxLevel: 10, // 最多10级(最多11个研究队列和11个舰队槽位)
|
||||||
requirements: { [BuildingType.ResearchLab]: 1 },
|
requirements: { [BuildingType.ResearchLab]: 1 },
|
||||||
levelRequirements: {
|
levelRequirements: {
|
||||||
3: { [BuildingType.ResearchLab]: 5 },
|
3: { [BuildingType.ResearchLab]: 5 },
|
||||||
@@ -445,7 +460,8 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
[TechnologyType.EspionageTechnology]: {
|
[TechnologyType.EspionageTechnology]: {
|
||||||
id: TechnologyType.EspionageTechnology,
|
id: TechnologyType.EspionageTechnology,
|
||||||
name: '间谍技术',
|
name: '间谍技术',
|
||||||
description: '提高间谍探测效果,每级提高1级侦查深度',
|
description:
|
||||||
|
'提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||||
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
baseCost: { metal: 200, crystal: 1000, deuterium: 200, darkMatter: 0, energy: 0 },
|
||||||
baseTime: 60,
|
baseTime: 60,
|
||||||
costMultiplier: 2,
|
costMultiplier: 2,
|
||||||
@@ -515,7 +531,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
id: TechnologyType.GravitonTechnology,
|
id: TechnologyType.GravitonTechnology,
|
||||||
name: '引力技术',
|
name: '引力技术',
|
||||||
description: '研究引力操纵,死星的必要技术',
|
description: '研究引力操纵,死星的必要技术',
|
||||||
baseCost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 300000, energy: 0 },
|
baseCost: { metal: 0, crystal: 0, deuterium: 0, darkMatter: 100000, energy: 0 },
|
||||||
baseTime: 0,
|
baseTime: 0,
|
||||||
costMultiplier: 3,
|
costMultiplier: 3,
|
||||||
maxLevel: 1, // 只有1级
|
maxLevel: 1, // 只有1级
|
||||||
@@ -605,7 +621,7 @@ export const TECHNOLOGIES: Record<TechnologyType, TechnologyConfig> = {
|
|||||||
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 200000, energy: 0 },
|
baseCost: { metal: 4000000, crystal: 8000000, deuterium: 4000000, darkMatter: 200000, energy: 0 },
|
||||||
baseTime: 300,
|
baseTime: 300,
|
||||||
costMultiplier: 2,
|
costMultiplier: 2,
|
||||||
maxLevel: 5, // 最多5级
|
maxLevel: 10,
|
||||||
requirements: {
|
requirements: {
|
||||||
[BuildingType.ResearchLab]: 12,
|
[BuildingType.ResearchLab]: 12,
|
||||||
[TechnologyType.HyperspaceTechnology]: 8,
|
[TechnologyType.HyperspaceTechnology]: 8,
|
||||||
@@ -674,13 +690,13 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
|||||||
[ShipType.Battleship]: {
|
[ShipType.Battleship]: {
|
||||||
id: ShipType.Battleship,
|
id: ShipType.Battleship,
|
||||||
name: '战列舰',
|
name: '战列舰',
|
||||||
description: '重型战舰',
|
description: '重型战舰,主力作战单位',
|
||||||
cost: { metal: 45000, crystal: 15000, deuterium: 0, darkMatter: 0, energy: 0 },
|
cost: { metal: 45000, crystal: 15000, deuterium: 0, darkMatter: 0, energy: 0 },
|
||||||
buildTime: 90,
|
buildTime: 90,
|
||||||
cargoCapacity: 1500,
|
cargoCapacity: 1500,
|
||||||
attack: 1000,
|
attack: 1200,
|
||||||
shield: 200,
|
shield: 300,
|
||||||
armor: 6000,
|
armor: 10000,
|
||||||
speed: 10000,
|
speed: 10000,
|
||||||
fuelConsumption: 500,
|
fuelConsumption: 500,
|
||||||
storageUsage: 25,
|
storageUsage: 25,
|
||||||
@@ -728,13 +744,13 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
|||||||
[ShipType.Destroyer]: {
|
[ShipType.Destroyer]: {
|
||||||
id: ShipType.Destroyer,
|
id: ShipType.Destroyer,
|
||||||
name: '驱逐舰',
|
name: '驱逐舰',
|
||||||
description: '擅长摧毁大型舰船的猎杀者',
|
description: '专业反大型舰船战舰,高火力低防护',
|
||||||
cost: { metal: 60000, crystal: 50000, deuterium: 15000, darkMatter: 0, energy: 0 },
|
cost: { metal: 60000, crystal: 50000, deuterium: 15000, darkMatter: 0, energy: 0 },
|
||||||
buildTime: 120,
|
buildTime: 120,
|
||||||
cargoCapacity: 2000,
|
cargoCapacity: 2000,
|
||||||
attack: 2000,
|
attack: 2500,
|
||||||
shield: 500,
|
shield: 250,
|
||||||
armor: 11000,
|
armor: 8000,
|
||||||
speed: 5000,
|
speed: 5000,
|
||||||
fuelConsumption: 1000,
|
fuelConsumption: 1000,
|
||||||
storageUsage: 40,
|
storageUsage: 40,
|
||||||
@@ -829,7 +845,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
|||||||
attack: 1,
|
attack: 1,
|
||||||
shield: 1,
|
shield: 1,
|
||||||
armor: 200,
|
armor: 200,
|
||||||
speed: 0,
|
speed: 1, // 极低速度,可被舰队携带但非常慢
|
||||||
fuelConsumption: 0,
|
fuelConsumption: 0,
|
||||||
storageUsage: 1,
|
storageUsage: 1,
|
||||||
requirements: { [BuildingType.Shipyard]: 1 }
|
requirements: { [BuildingType.Shipyard]: 1 }
|
||||||
@@ -857,7 +873,7 @@ export const SHIPS: Record<ShipType, ShipConfig> = {
|
|||||||
id: ShipType.Deathstar,
|
id: ShipType.Deathstar,
|
||||||
name: '死星',
|
name: '死星',
|
||||||
description: '终极武器,能够摧毁整个行星',
|
description: '终极武器,能够摧毁整个行星',
|
||||||
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 50000, energy: 0 },
|
cost: { metal: 5000000, crystal: 4000000, deuterium: 1000000, darkMatter: 20000, energy: 0 },
|
||||||
buildTime: 600,
|
buildTime: 600,
|
||||||
cargoCapacity: 1000000,
|
cargoCapacity: 1000000,
|
||||||
attack: 200000,
|
attack: 200000,
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import type { ClassValue } from 'clsx'
|
|||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export const cn = (...inputs: ClassValue[]) => {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: 'Erobere die Sterne',
|
||||||
|
startGame: 'Spiel starten',
|
||||||
|
privacyAgreement: 'Datenschutzvereinbarung',
|
||||||
|
privacyAgreementDesc: 'Bitte lesen und akzeptieren Sie unsere Datenschutzrichtlinie, bevor Sie das Spiel starten.',
|
||||||
|
agreeToPrivacy: 'Ich habe gelesen und stimme zu',
|
||||||
|
viewFullPolicy: 'Vollständige Richtlinie anzeigen',
|
||||||
|
agreeAndStart: 'Zustimmen & Starten'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: 'Bestätigen',
|
confirm: 'Bestätigen',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
@@ -8,6 +17,7 @@ export default {
|
|||||||
close: 'Schließen',
|
close: 'Schließen',
|
||||||
back: 'Zurück',
|
back: 'Zurück',
|
||||||
next: 'Weiter',
|
next: 'Weiter',
|
||||||
|
gotIt: '',
|
||||||
previous: 'Vorherige',
|
previous: 'Vorherige',
|
||||||
submit: 'Absenden',
|
submit: 'Absenden',
|
||||||
reset: 'Zurücksetzen',
|
reset: 'Zurücksetzen',
|
||||||
@@ -31,10 +41,12 @@ export default {
|
|||||||
goToBuildings: 'Zu Gebäuden',
|
goToBuildings: 'Zu Gebäuden',
|
||||||
locked: 'Gesperrt',
|
locked: 'Gesperrt',
|
||||||
viewRequirements: 'Anforderungen anzeigen',
|
viewRequirements: 'Anforderungen anzeigen',
|
||||||
|
requirements: 'Anforderungen',
|
||||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||||
current: 'Aktuell',
|
current: 'Aktuell',
|
||||||
level: 'Stufe',
|
level: 'Stufe',
|
||||||
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.'
|
gmModeActivated: 'GM-Modus aktiviert! Überprüfen Sie das Navigationsmenü.',
|
||||||
|
view: 'Anzeigen'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||||
@@ -56,7 +68,8 @@ export default {
|
|||||||
buildingLevelZero: 'Gebäudelevel ist 0, kann nicht abgerissen werden',
|
buildingLevelZero: 'Gebäudelevel ist 0, kann nicht abgerissen werden',
|
||||||
researchQueueFull: 'Forschungsauftrag voll',
|
researchQueueFull: 'Forschungsauftrag voll',
|
||||||
moonExists: 'Mond existiert bereits',
|
moonExists: 'Mond existiert bereits',
|
||||||
insufficientDebris: 'Unzureichendes Trümmerfeld'
|
insufficientDebris: 'Unzureichendes Trümmerfeld',
|
||||||
|
launchFailed: 'Start fehlgeschlagen'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: 'Übersicht',
|
overview: 'Übersicht',
|
||||||
@@ -111,7 +124,13 @@ export default {
|
|||||||
homePlanet: 'Heimatplanet',
|
homePlanet: 'Heimatplanet',
|
||||||
planetPrefix: 'Planet',
|
planetPrefix: 'Planet',
|
||||||
moonSuffix: 's Mond',
|
moonSuffix: 's Mond',
|
||||||
colonyPrefix: 'Kolonie'
|
colonyPrefix: 'Kolonie',
|
||||||
|
renamePlanet: 'Planet umbenennen',
|
||||||
|
renamePlanetTitle: 'Planet umbenennen',
|
||||||
|
newPlanetName: 'Neuer Name',
|
||||||
|
planetNamePlaceholder: 'Neuen Planetennamen eingeben',
|
||||||
|
rename: 'Umbenennen',
|
||||||
|
renameSuccess: 'Planet wurde in {name} umbenannt'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: 'Gesamtpunkte'
|
points: 'Gesamtpunkte'
|
||||||
@@ -125,6 +144,7 @@ export default {
|
|||||||
roboticsFactory: 'Roboterfabrik',
|
roboticsFactory: 'Roboterfabrik',
|
||||||
naniteFactory: 'Nanitenfabrik',
|
naniteFactory: 'Nanitenfabrik',
|
||||||
shipyard: 'Raumschiffwerft',
|
shipyard: 'Raumschiffwerft',
|
||||||
|
hangar: 'Hangar',
|
||||||
researchLab: 'Forschungslabor',
|
researchLab: 'Forschungslabor',
|
||||||
metalStorage: 'Metallspeicher',
|
metalStorage: 'Metallspeicher',
|
||||||
crystalStorage: 'Kristallspeicher',
|
crystalStorage: 'Kristallspeicher',
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
jumpGate: 'Sprungtor',
|
jumpGate: 'Sprungtor',
|
||||||
planetDestroyerFactory: 'Planetenzerstörer-Fabrik',
|
planetDestroyerFactory: 'Planetenzerstörer-Fabrik',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
production: 'Produktion',
|
production: 'Produktion',
|
||||||
consumption: 'Verbrauch',
|
consumption: 'Verbrauch',
|
||||||
totalCost: 'Gesamtkosten',
|
totalCost: 'Gesamtkosten',
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
|
roboticsFactory: 'Beschleunigt Baugeschwindigkeit',
|
||||||
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
naniteFactory: 'Erhöht Bauauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
||||||
shipyard: 'Baut Schiffe',
|
shipyard: 'Baut Schiffe',
|
||||||
|
hangar: 'Spezialisierte Einrichtung zur Erweiterung der Flottenspeicherkapazität, unterstützt Planetenspezialisierung',
|
||||||
researchLab: 'Erforscht Technologien',
|
researchLab: 'Erforscht Technologien',
|
||||||
metalStorage: 'Erhöht Metallspeicherkapazität',
|
metalStorage: 'Erhöht Metallspeicherkapazität',
|
||||||
crystalStorage: 'Erhöht Kristallspeicherkapazität',
|
crystalStorage: 'Erhöht Kristallspeicherkapazität',
|
||||||
@@ -199,10 +221,10 @@ export default {
|
|||||||
lightFighter: 'Grundlegende Kampfeinheit',
|
lightFighter: 'Grundlegende Kampfeinheit',
|
||||||
heavyFighter: 'Schwer gepanzerter Jäger',
|
heavyFighter: 'Schwer gepanzerter Jäger',
|
||||||
cruiser: 'Mittleres Kriegsschiff, ausgewogene Offensive und Defensive',
|
cruiser: 'Mittleres Kriegsschiff, ausgewogene Offensive und Defensive',
|
||||||
battleship: 'Mächtiges Kriegsschiff',
|
battleship: 'Schweres Hauptkriegsschiff mit starker Feuerkraft und hoher Verteidigung',
|
||||||
battlecruiser: 'Schnelles mächtiges Kriegsschiff, hervorragend gegen Schlachtschiffe',
|
battlecruiser: 'Schnelles mächtiges Kriegsschiff, hervorragend gegen Schlachtschiffe',
|
||||||
bomber: 'Spezialisiertes Schiff zum Angriff auf Verteidigungsanlagen',
|
bomber: 'Spezialisiertes Schiff zum Angriff auf Verteidigungsanlagen',
|
||||||
destroyer: 'Jäger spezialisiert auf Zerstörung großer Schiffe',
|
destroyer: 'Spezialisiertes Anti-Großschiff mit hoher Feuerkraft aber geringer Verteidigung',
|
||||||
smallCargo: 'Transportiert kleine Mengen Ressourcen',
|
smallCargo: 'Transportiert kleine Mengen Ressourcen',
|
||||||
largeCargo: 'Transportiert große Mengen Ressourcen',
|
largeCargo: 'Transportiert große Mengen Ressourcen',
|
||||||
colonyShip: 'Zur Kolonisierung neuer Planeten',
|
colonyShip: 'Zur Kolonisierung neuer Planeten',
|
||||||
@@ -278,8 +300,9 @@ export default {
|
|||||||
ionTechnology: 'Ionenwaffentechnologie',
|
ionTechnology: 'Ionenwaffentechnologie',
|
||||||
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
hyperspaceTechnology: 'Hyperraumsprung-Technologie',
|
||||||
plasmaTechnology: 'Plasmawaffentechnologie',
|
plasmaTechnology: 'Plasmawaffentechnologie',
|
||||||
computerTechnology: 'Erhöht Forschungsauftragskapazität, +1 pro Stufe (max 10 Stufen)',
|
computerTechnology: 'Erhöht Forschungswarteschlange und Flottenmissionsslots, +1 Warteschlange +1 Slot pro Stufe (max 10 Stufen)',
|
||||||
espionageTechnology: 'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe',
|
espionageTechnology:
|
||||||
|
'Verbessert Sondenwirksamkeit, +1 Spionagestufe pro Stufe. Spionagestufe = eigene Stufe - Gegnerstufe + Sonden/5. ≥-1 zeigt Flotte, ≥1 zeigt Verteidigung, ≥3 zeigt Gebäude, ≥5 zeigt Technologien',
|
||||||
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
weaponsTechnology: 'Erhöht Angriffskraft von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
shieldingTechnology: 'Erhöht Schilde von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
armourTechnology: 'Erhöht Panzerung von Schiffen und Verteidigung um 10% pro Stufe',
|
||||||
@@ -312,10 +335,13 @@ export default {
|
|||||||
darkMatterSpecialist: 'Verbessert Dunkle-Materie-Sammlungseffizienz'
|
darkMatterSpecialist: 'Verbessert Dunkle-Materie-Sammlungseffizienz'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: 'Aktive Aufgaben',
|
||||||
|
empty: 'Keine aktiven Warteschlangen',
|
||||||
buildQueue: 'Bauauftrag',
|
buildQueue: 'Bauauftrag',
|
||||||
researchQueue: 'Forschungsauftrag',
|
researchQueue: 'Forschungsauftrag',
|
||||||
building: 'Im Bau',
|
building: 'Im Bau',
|
||||||
researching: 'In Forschung',
|
researching: 'In Forschung',
|
||||||
|
demolishing: 'Wird abgerissen',
|
||||||
remaining: 'Verbleibend',
|
remaining: 'Verbleibend',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
cancelBuild: 'Bau abbrechen',
|
cancelBuild: 'Bau abbrechen',
|
||||||
@@ -323,7 +349,14 @@ export default {
|
|||||||
confirmCancel: 'Möchten Sie wirklich abbrechen? 50% der Ressourcen werden zurückerstattet.',
|
confirmCancel: 'Möchten Sie wirklich abbrechen? 50% der Ressourcen werden zurückerstattet.',
|
||||||
level: 'Stufe',
|
level: 'Stufe',
|
||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeToLevel: 'Auf Stufe aufrüsten'
|
upgradeToLevel: 'Auf Stufe aufrüsten',
|
||||||
|
tabs: {
|
||||||
|
all: 'Alle',
|
||||||
|
buildings: 'Gebäude',
|
||||||
|
research: 'Forschung',
|
||||||
|
ships: 'Schiffe',
|
||||||
|
defense: 'Verteidigung'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: 'Planetenübersicht',
|
title: 'Planetenübersicht',
|
||||||
@@ -346,6 +379,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'Ausbaukosten',
|
upgradeCost: 'Ausbaukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: 'Bauen',
|
||||||
upgrade: 'Ausbauen',
|
upgrade: 'Ausbauen',
|
||||||
maxLevelReached: 'Maximale Stufe erreicht',
|
maxLevelReached: 'Maximale Stufe erreicht',
|
||||||
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
requirementsNotMet: 'Anforderungen nicht erfüllt',
|
||||||
@@ -377,6 +411,7 @@ export default {
|
|||||||
fuelConsumption: 'Treibstoffverbrauch',
|
fuelConsumption: 'Treibstoffverbrauch',
|
||||||
buildCost: 'Baukosten',
|
buildCost: 'Baukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
perUnit: 'Pro Einheit',
|
perUnit: 'Pro Einheit',
|
||||||
batchCalculator: 'Batch-Rechner',
|
batchCalculator: 'Batch-Rechner',
|
||||||
quantity: 'Menge',
|
quantity: 'Menge',
|
||||||
@@ -407,6 +442,7 @@ export default {
|
|||||||
armor: 'Panzerung',
|
armor: 'Panzerung',
|
||||||
buildCost: 'Baukosten',
|
buildCost: 'Baukosten',
|
||||||
buildTime: 'Bauzeit',
|
buildTime: 'Bauzeit',
|
||||||
|
build: '',
|
||||||
perUnit: 'Pro Einheit',
|
perUnit: 'Pro Einheit',
|
||||||
batchCalculator: 'Batch-Rechner',
|
batchCalculator: 'Batch-Rechner',
|
||||||
quantity: 'Menge',
|
quantity: 'Menge',
|
||||||
@@ -463,6 +499,7 @@ export default {
|
|||||||
colonize: 'Kolonisieren',
|
colonize: 'Kolonisieren',
|
||||||
spy: 'Spionage',
|
spy: 'Spionage',
|
||||||
deploy: 'Stationieren',
|
deploy: 'Stationieren',
|
||||||
|
expedition: 'Expedition',
|
||||||
recycle: 'Recyceln',
|
recycle: 'Recyceln',
|
||||||
transportResources: 'Ressourcen transportieren',
|
transportResources: 'Ressourcen transportieren',
|
||||||
totalCargoCapacity: 'Gesamtladekapazität',
|
totalCargoCapacity: 'Gesamtladekapazität',
|
||||||
@@ -580,18 +617,21 @@ export default {
|
|||||||
debris: 'Trümmer',
|
debris: 'Trümmer',
|
||||||
giftPlanetTitle: 'Geschenk senden',
|
giftPlanetTitle: 'Geschenk senden',
|
||||||
giftPlanetMessage:
|
giftPlanetMessage:
|
||||||
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.'
|
'Möchten Sie wirklich Ressourcen als Geschenk an Planet [{coordinates}] senden?\n\nBitte gehen Sie zur Flottenseite, um Transporter auszuwählen und Ressourcen zu laden.',
|
||||||
|
npcPlanetName: '{name}s Planet'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: 'Nachrichten',
|
title: 'Nachrichten',
|
||||||
battles: 'Kämpfe',
|
battles: 'Kämpfe',
|
||||||
spy: 'Spionage',
|
spy: 'Spionage',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
spied: 'Ausspioniert',
|
spied: 'Ausspioniert',
|
||||||
battleReports: 'Kampfberichte',
|
battleReports: 'Kampfberichte',
|
||||||
spyReports: 'Spionageberichte',
|
spyReports: 'Spionageberichte',
|
||||||
noBattleReports: 'Keine Kampfberichte',
|
noBattleReports: 'Keine Kampfberichte',
|
||||||
noSpyReports: 'Keine Spionageberichte',
|
noSpyReports: 'Keine Spionageberichte',
|
||||||
|
noDiplomaticReports: '',
|
||||||
noSpiedNotifications: 'Keine Ausspionierungs-Benachrichtigungen',
|
noSpiedNotifications: 'Keine Ausspionierungs-Benachrichtigungen',
|
||||||
battleReport: 'Kampfbericht',
|
battleReport: 'Kampfbericht',
|
||||||
spyReport: 'Spionagebericht',
|
spyReport: 'Spionagebericht',
|
||||||
@@ -617,6 +657,7 @@ export default {
|
|||||||
targetPlanet: 'Zielplanet',
|
targetPlanet: 'Zielplanet',
|
||||||
attackerRemaining: 'Angreifer verblieben',
|
attackerRemaining: 'Angreifer verblieben',
|
||||||
defenderRemaining: 'Verteidiger verblieben',
|
defenderRemaining: 'Verteidiger verblieben',
|
||||||
|
allDestroyed: 'Alle zerstört',
|
||||||
moonChance: 'Mondchance',
|
moonChance: 'Mondchance',
|
||||||
showRoundDetails: 'Rundendetails anzeigen',
|
showRoundDetails: 'Rundendetails anzeigen',
|
||||||
hideRoundDetails: 'Rundendetails ausblenden',
|
hideRoundDetails: 'Rundendetails ausblenden',
|
||||||
@@ -646,7 +687,48 @@ export default {
|
|||||||
hostile: 'Sie sind feindlich und nehmen keine Geschenke an',
|
hostile: 'Sie sind feindlich und nehmen keine Geschenke an',
|
||||||
neutral_distrust: 'Sie vertrauen Ihnen nicht',
|
neutral_distrust: 'Sie vertrauen Ihnen nicht',
|
||||||
polite_decline: 'Sie lehnten höflich ab'
|
polite_decline: 'Sie lehnten höflich ab'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: '',
|
||||||
|
spyDetected: '',
|
||||||
|
detectionResult: '',
|
||||||
|
detectionSuccess: '',
|
||||||
|
spiedNotificationMessage: '',
|
||||||
|
spiedNotificationTip: '',
|
||||||
|
viewInGalaxy: '',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: '',
|
||||||
|
missionSuccess: '',
|
||||||
|
missionFailed: '',
|
||||||
|
origin: '',
|
||||||
|
destination: '',
|
||||||
|
missionDetails: '',
|
||||||
|
transportedResources: '',
|
||||||
|
recycledResources: '',
|
||||||
|
remainingDebris: '',
|
||||||
|
newPlanet: '',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: '',
|
||||||
|
activityType: {
|
||||||
|
recycle: ''
|
||||||
|
},
|
||||||
|
activityLocation: '',
|
||||||
|
position: '',
|
||||||
|
nearPlanet: '',
|
||||||
|
activityDescription: '',
|
||||||
|
npcActivityMessage: '',
|
||||||
|
arrivalTime: '',
|
||||||
|
npcActivityTip: '',
|
||||||
|
clearMessages: 'Nachrichten löschen',
|
||||||
|
clearMessageTypes: 'Nachrichtentypen zum Löschen auswählen',
|
||||||
|
clearBattleReports: 'Kampfberichte',
|
||||||
|
clearSpyReports: 'Spionageberichte',
|
||||||
|
clearSpiedNotifications: 'Spionagebenachrichtigungen',
|
||||||
|
clearMissionReports: 'Missionsberichte',
|
||||||
|
clearNPCActivity: 'NPC-Aktivität',
|
||||||
|
clearGiftNotifications: 'Geschenkbenachrichtigungen',
|
||||||
|
clearGiftRejected: 'Abgelehnte Geschenke',
|
||||||
|
clearNow: 'Jetzt löschen'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
|
transportSuccess: 'Transportmission erfolgreich abgeschlossen',
|
||||||
@@ -658,7 +740,19 @@ export default {
|
|||||||
recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen',
|
recycleSuccess: 'Recyclingmission erfolgreich abgeschlossen',
|
||||||
recycleFailed: 'Recyclingmission fehlgeschlagen, keine Trümmer am Zielort',
|
recycleFailed: 'Recyclingmission fehlgeschlagen, keine Trümmer am Zielort',
|
||||||
destroySuccess: 'Planetenzerstörungsmission erfolgreich ausgeführt',
|
destroySuccess: 'Planetenzerstörungsmission erfolgreich ausgeführt',
|
||||||
destroyFailed: 'Planetenzerstörungsmission fehlgeschlagen'
|
destroyFailed: 'Planetenzerstörungsmission fehlgeschlagen',
|
||||||
|
missileAttackSuccess: 'Raketenangriff erfolgreich',
|
||||||
|
missileAttackFailed: 'Raketenangriff fehlgeschlagen, Zielplanet existiert nicht',
|
||||||
|
missileAttackIntercepted: 'Alle Raketen abgefangen',
|
||||||
|
hits: 'Treffer',
|
||||||
|
expeditionResources: 'Die Expedition hat Ressourcen gefunden!',
|
||||||
|
expeditionDarkMatter: 'Die Expedition hat Dunkle Materie gefunden!',
|
||||||
|
expeditionFleet: 'Die Expedition hat verlassene Schiffe gefunden!',
|
||||||
|
expeditionPiratesAttack: 'Die Expedition wurde von Piraten angegriffen und verlor einige Schiffe',
|
||||||
|
expeditionPiratesEscaped: 'Die Expedition traf auf Piraten, konnte aber entkommen',
|
||||||
|
expeditionAliensAttack: 'Die Expedition wurde von Aliens angegriffen und verlor einige Schiffe',
|
||||||
|
expeditionAliensEscaped: 'Die Expedition traf auf Aliens, konnte aber entkommen',
|
||||||
|
expeditionNothing: 'Die Expedition hat nichts gefunden'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: 'Kampfsimulator',
|
title: 'Kampfsimulator',
|
||||||
@@ -750,7 +844,27 @@ export default {
|
|||||||
buildDate: 'Build-Datum',
|
buildDate: 'Build-Datum',
|
||||||
community: 'Community',
|
community: 'Community',
|
||||||
github: 'GitHub-Repository',
|
github: 'GitHub-Repository',
|
||||||
qqGroup: 'QQ-Gruppe'
|
qqGroup: 'QQ-Gruppe',
|
||||||
|
privacyPolicy: 'Datenschutzrichtlinie',
|
||||||
|
notifications: 'Benachrichtigungseinstellungen',
|
||||||
|
notificationsDesc: 'Verwalten Sie Benachrichtigungen im Spiel',
|
||||||
|
notificationTypes: 'Benachrichtigungstypen',
|
||||||
|
browserNotifications: 'Browser-Benachrichtigungen',
|
||||||
|
inAppNotifications: 'In-App-Benachrichtigungen',
|
||||||
|
constructionComplete: 'Bau abgeschlossen',
|
||||||
|
researchComplete: 'Forschung abgeschlossen',
|
||||||
|
browserPermission: 'Browser-Benachrichtigungen aktivieren',
|
||||||
|
permissionGranted: 'Erlaubnis erteilt',
|
||||||
|
permissionDenied: 'Erlaubnis verweigert/nicht erteilt',
|
||||||
|
inAppNotificationsDesc: 'Über In-Page-Popups anzeigen',
|
||||||
|
notificationsDisabled: 'Aktivieren Sie einen der obigen Schalter, um spezifische Benachrichtigungen zu konfigurieren',
|
||||||
|
suppressInFocus: 'Browser-Benachrichtigungen unterdrücken, wenn Seite fokussiert ist',
|
||||||
|
expandTypes: 'Details anzeigen',
|
||||||
|
collapseTypes: 'Details ausblenden'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: 'Bau abgeschlossen',
|
||||||
|
researchComplete: 'Forschung abgeschlossen'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM-Kontrollpanel',
|
title: 'GM-Kontrollpanel',
|
||||||
@@ -802,7 +916,8 @@ export default {
|
|||||||
npcWillSpyAndAttack: '{npcName} wird in 5s spionieren und in 10s angreifen',
|
npcWillSpyAndAttack: '{npcName} wird in 5s spionieren und in 10s angreifen',
|
||||||
acceleratedMissions: '{count} Missionen auf 5 Sekunden beschleunigt',
|
acceleratedMissions: '{count} Missionen auf 5 Sekunden beschleunigt',
|
||||||
npcFleetInitialized: '{npcName} Flotte initialisiert',
|
npcFleetInitialized: '{npcName} Flotte initialisiert',
|
||||||
npcFleetDetails: '100 Spionagesonden\n500 Leichte Jäger\n300 Schwere Jäger\n200 Kreuzer\n100 Schlachtschiffe\n50 Bomber\n30 Zerstörer\n20 Schlachtkreuzer',
|
npcFleetDetails:
|
||||||
|
'100 Spionagesonden\n500 Leichte Jäger\n300 Schwere Jäger\n200 Kreuzer\n100 Schlachtschiffe\n50 Bomber\n30 Zerstörer\n20 Schlachtkreuzer',
|
||||||
dangerZone: 'Gefahrenzone',
|
dangerZone: 'Gefahrenzone',
|
||||||
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
|
dangerZoneDesc: 'Die folgenden Vorgänge sind irreversibel',
|
||||||
resetGame: 'Spiel zurücksetzen',
|
resetGame: 'Spiel zurücksetzen',
|
||||||
@@ -813,17 +928,42 @@ export default {
|
|||||||
completeQueuesSuccess: ''
|
completeQueuesSuccess: ''
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count} feindliche Flotten im Anmarsch',
|
||||||
npcSpyIncoming: 'NPC-Spionagesonde nähert sich',
|
npcSpyIncoming: 'NPC-Spionagesonde nähert sich',
|
||||||
npcAttackIncoming: 'NPC-Flotten-Angriff im Anmarsch!',
|
npcAttackIncoming: 'NPC-Flotten-Angriff im Anmarsch!',
|
||||||
npcFleetIncoming: 'NPC-Flotte nähert sich',
|
npcFleetIncoming: 'NPC-Flotte nähert sich',
|
||||||
ships: 'Schiffe',
|
ships: 'Schiffe',
|
||||||
spiedBy: 'Ausspioniert von',
|
spiedBy: 'Ausspioniert von',
|
||||||
attackedBy: 'Angegriffen von',
|
attackedBy: 'Angegriffen von',
|
||||||
detectionSuccess: 'Spionage entdeckt',
|
detectionSuccess: 'Du hast feindliche Spionage entdeckt!',
|
||||||
detectionFailed: 'Spionage nicht entdeckt',
|
detectionFailed: 'Spionage nicht entdeckt',
|
||||||
npcSpiedYourPlanet: 'NPC hat deinen Planeten ausspioniert',
|
npcSpiedYourPlanet: 'NPC hat deinen Planeten ausspioniert',
|
||||||
npcAttackedYourPlanet: 'NPC hat deinen Planeten angegriffen'
|
npcAttackedYourPlanet: 'NPC hat deinen Planeten angegriffen'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: 'Feindalarm',
|
||||||
|
markAllRead: 'Alle gelesen',
|
||||||
|
noAlerts: 'Keine Alarme',
|
||||||
|
fleetSize: 'Flottengröße',
|
||||||
|
ships: 'Schiffe',
|
||||||
|
viewFleet: 'Flotte anzeigen',
|
||||||
|
alertDetails: 'Alarmdetails',
|
||||||
|
targetInfo: 'Zielinfo',
|
||||||
|
arrivalTime: 'Ankunftszeit',
|
||||||
|
countdown: 'Countdown',
|
||||||
|
viewMessages: 'Nachrichten anzeigen',
|
||||||
|
arrived: 'Angekommen',
|
||||||
|
missionType: {
|
||||||
|
spy: 'Spionage',
|
||||||
|
attack: 'Angriff',
|
||||||
|
unknown: 'Unbekannt'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: 'Feindliche Spionage im Anmarsch!',
|
||||||
|
attack: 'Feindlicher Angriff im Anmarsch!',
|
||||||
|
unknown: 'Feindliche Flotte im Anmarsch!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: 'Diplomatie',
|
title: 'Diplomatie',
|
||||||
description: 'Verwalte diplomatische Beziehungen mit NPCs',
|
description: 'Verwalte diplomatische Beziehungen mit NPCs',
|
||||||
@@ -840,6 +980,10 @@ export default {
|
|||||||
recentEvents: 'Aktuelle Ereignisse',
|
recentEvents: 'Aktuelle Ereignisse',
|
||||||
recentEventsDescription: 'Protokoll der jüngsten diplomatischen Aktivitäten',
|
recentEventsDescription: 'Protokoll der jüngsten diplomatischen Aktivitäten',
|
||||||
ago: 'vor',
|
ago: 'vor',
|
||||||
|
notifications: '',
|
||||||
|
markAllRead: '',
|
||||||
|
noReports: '',
|
||||||
|
viewAll: '',
|
||||||
status: {
|
status: {
|
||||||
friendly: 'Freundlich',
|
friendly: 'Freundlich',
|
||||||
neutral: 'Neutral',
|
neutral: 'Neutral',
|
||||||
@@ -852,9 +996,30 @@ export default {
|
|||||||
more: 'weitere',
|
more: 'weitere',
|
||||||
actions: {
|
actions: {
|
||||||
gift: 'Geschenk senden',
|
gift: 'Geschenk senden',
|
||||||
viewPlanets: 'Planeten ansehen'
|
viewPlanets: 'Planeten ansehen',
|
||||||
|
addNote: 'Notiz hinzufügen',
|
||||||
|
editNote: 'Notiz bearbeiten'
|
||||||
},
|
},
|
||||||
|
note: 'Notiz',
|
||||||
|
notePlaceholder: 'Notiz eingeben...',
|
||||||
|
noteEmpty: 'Keine Notiz',
|
||||||
lastEvent: 'Letztes Ereignis',
|
lastEvent: 'Letztes Ereignis',
|
||||||
|
reportDetails: 'Diplomatischer Bericht Details',
|
||||||
|
eventDescription: 'Ereignisbeschreibung',
|
||||||
|
reputationChange: 'Ansehensänderung',
|
||||||
|
before: 'Vorher',
|
||||||
|
after: 'Nachher',
|
||||||
|
statusChange: 'Statusänderung',
|
||||||
|
viewDiplomacy: 'Diplomatie-Seite anzeigen',
|
||||||
|
eventType: {
|
||||||
|
gift: 'Ressourcen geschenkt',
|
||||||
|
attack: 'Angriff gestartet',
|
||||||
|
allyAttacked: 'Verbündeten angegriffen',
|
||||||
|
spy: 'Spionage durchgeführt',
|
||||||
|
stealDebris: 'Trümmer gestohlen',
|
||||||
|
destroyPlanet: 'Planet zerstört',
|
||||||
|
unknown: 'Unbekanntes Ereignis'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: 'Geschenk gesendet',
|
gift: 'Geschenk gesendet',
|
||||||
attack: 'Angriff',
|
attack: 'Angriff',
|
||||||
@@ -883,12 +1048,64 @@ export default {
|
|||||||
receivedGiftFromNpc: 'Geschenk von {npcName} erhalten',
|
receivedGiftFromNpc: 'Geschenk von {npcName} erhalten',
|
||||||
acceptedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} angenommen: {metal}M {crystal}K {deuterium}D',
|
acceptedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} angenommen: {metal}M {crystal}K {deuterium}D',
|
||||||
playerRejectedGift: 'Spieler hat Geschenk abgelehnt',
|
playerRejectedGift: 'Spieler hat Geschenk abgelehnt',
|
||||||
rejectedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} abgelehnt. Ansehen {reputation}'
|
rejectedGiftFromNpc: 'Sie haben ein Geschenk von {npcName} abgelehnt. Ansehen {reputation}',
|
||||||
|
destroyedNpcPlanet: '{npcName}s {planetName} zerstört',
|
||||||
|
playerDestroyedPlanet: 'Spieler hat {planetName} zerstört',
|
||||||
|
youDestroyedNpcPlanet: 'Sie haben {npcName}s {planetName} zerstört. Ansehen {reputation}',
|
||||||
|
playerDestroyedAllyPlanet: 'Spieler hat Verbündeten {allyName}s {planetName} zerstört',
|
||||||
|
allyOutraged: '{allyName} ist empört, dass Sie den Planeten {planetName} ihres Verbündeten {targetName} zerstört haben',
|
||||||
|
npcEliminated: 'NPC {npcName} wurde vollständig eliminiert',
|
||||||
|
npcEliminatedMessage: 'Sie haben alle Planeten von {npcName} zerstört! Diese Fraktion wurde vollständig ausgelöscht.'
|
||||||
|
},
|
||||||
|
searchPlaceholder: 'NPC-Name suchen...',
|
||||||
|
viewMode: {
|
||||||
|
card: 'Karte',
|
||||||
|
list: 'Liste'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC-Diagnose',
|
||||||
|
title: 'NPC-Statusdiagnose',
|
||||||
|
description:
|
||||||
|
'Spielerpunkte: {points}, Spionageintervall: {spyInterval}Min, Angriffsintervall: {attackInterval}Min, Angriffswahrscheinlichkeit: {attackProb}%',
|
||||||
|
noData: 'Keine NPC-Daten',
|
||||||
|
difficulty: 'Schwierigkeit',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: 'Einfach',
|
||||||
|
medium: 'Mittel',
|
||||||
|
hard: 'Schwer'
|
||||||
|
},
|
||||||
|
reputation: 'Ansehen',
|
||||||
|
spyProbes: 'Spionagesonden',
|
||||||
|
fleetPower: 'Flottenstärke',
|
||||||
|
canSpy: 'Kann spionieren',
|
||||||
|
canAttack: 'Kann angreifen',
|
||||||
|
attackProbability: 'Angriffswahrscheinlichkeit',
|
||||||
|
nextSpy: 'Nächste Spionage',
|
||||||
|
nextAttack: 'Nächster Angriff',
|
||||||
|
yes: 'Ja',
|
||||||
|
no: 'Nein',
|
||||||
|
timeFormat: '{min}m {sec}s',
|
||||||
|
anytime: 'Jederzeit',
|
||||||
|
statusExplanation: 'Statuserklärung',
|
||||||
|
noRelation: 'Keine Beziehung',
|
||||||
|
noRelationNeutral: 'Keine Beziehung (Neutral)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: 'Freundliche Beziehung, wird nicht handeln',
|
||||||
|
neutralNoAction: 'Neutrale Beziehung, wird nicht handeln',
|
||||||
|
hostileWillAct: 'Feindliche Beziehung, kann handeln',
|
||||||
|
noRelationNeutral: 'Keine diplomatische Beziehung, als neutral behandelt',
|
||||||
|
insufficientProbes: 'Unzureichende Sonden (Aktuell: {current}, Benötigt: {required})',
|
||||||
|
noFleet: 'Keine Kampfflotte',
|
||||||
|
spyCooldown: 'Spionage auf Abklingzeit ({min}m {sec}s)',
|
||||||
|
attackCooldown: 'Angriff auf Abklingzeit ({min}m {sec}s)',
|
||||||
|
notSpiedYet: 'Noch nicht spioniert, zuerst Spionage nötig'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: 'Vorherige',
|
previous: 'Vorherige',
|
||||||
next: 'Nächste',
|
next: 'Nächste',
|
||||||
|
gotIt: '',
|
||||||
first: 'Erste',
|
first: 'Erste',
|
||||||
last: 'Letzte',
|
last: 'Letzte',
|
||||||
page: 'Seite {page}'
|
page: 'Seite {page}'
|
||||||
@@ -897,5 +1114,159 @@ export default {
|
|||||||
title: 'Seite nicht gefunden',
|
title: 'Seite nicht gefunden',
|
||||||
description: 'Entschuldigung, die gesuchte Seite existiert nicht',
|
description: 'Entschuldigung, die gesuchte Seite existiert nicht',
|
||||||
goHome: 'Zur Startseite'
|
goHome: 'Zur Startseite'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: 'Datenschutzrichtlinie',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: 'Einführung',
|
||||||
|
content:
|
||||||
|
'Diese Datenschutzrichtlinie erklärt, wie OGame-Vue-Ts Ihre Daten behandelt. Wir sind dem Schutz Ihrer Privatsphäre verpflichtet, und dieses Spiel wurde mit vollständigem Respekt für die Privatsphäre der Benutzer entwickelt.'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: 'Datenerfassung',
|
||||||
|
content: 'Dieses Spiel erfasst und speichert nur die folgenden Daten lokal in Ihrem Browser:',
|
||||||
|
items: {
|
||||||
|
gameProgress: 'Spielfortschritt (Gebäudestufen, Flotten, Ressourcen usw.)',
|
||||||
|
settings: 'Spieleinstellungen (Benachrichtigungseinstellungen, Anzeigeoptionen usw.)',
|
||||||
|
language: 'Sprachpräferenz'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: 'Datenspeicherung',
|
||||||
|
content:
|
||||||
|
'Alle Daten werden im lokalen Speicher (localStorage) Ihres Browsers gespeichert. Dies bedeutet, dass Ihre Daten immer auf Ihrem eigenen Gerät verbleiben und wir keinen Zugriff auf Ihre Spieldaten haben, diese nicht einsehen oder erfassen können.'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: 'Keine Serverkommunikation',
|
||||||
|
content:
|
||||||
|
'Dieses Spiel ist ein vollständig offline funktionierendes Einzelspielerspiel. Abgesehen von der Update-Prüfungsfunktion (die Versionsinformationen von GitHub abruft) kommuniziert das Spiel mit keinem Server. Ihre Spieldaten verlassen niemals Ihr Gerät.'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: 'Drittanbieterdienste',
|
||||||
|
content:
|
||||||
|
'Dieses Spiel verwendet Analyse-Dienste von Drittanbietern, um Besucherstatistiken und Traffic-Quellen zu erfassen. Dies hilft uns, Nutzungsmuster zu verstehen und das Spielerlebnis zu verbessern. Diese Analysedaten sind anonym und enthalten keine persönlich identifizierbaren Informationen. Wir verwenden keine Werbedienste oder andere kommerzielle Tracking-Tools.'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: 'Datenkontrolle',
|
||||||
|
content: 'Sie haben die vollständige Kontrolle über Ihre Daten:',
|
||||||
|
items: {
|
||||||
|
export: 'Sie können Ihre Spieldaten jederzeit exportieren',
|
||||||
|
import: 'Sie können Daten aus Sicherungsdateien importieren',
|
||||||
|
delete: 'Sie können alle Daten löschen, indem Sie die Browserdaten löschen oder die Funktion "Daten löschen" im Spiel verwenden'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: 'Kontakt',
|
||||||
|
content: 'Wenn Sie Fragen zu dieser Datenschutzrichtlinie haben, kontaktieren Sie uns bitte über:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: 'Tage',
|
||||||
|
hours: 'Stunden',
|
||||||
|
minutes: 'Minuten',
|
||||||
|
seconds: 'Sekunden'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
welcome: {
|
||||||
|
title: 'Willkommen bei OGame',
|
||||||
|
content: 'Willkommen, Kommandant! Beginnen wir mit den Grundlagen und bauen Sie Ihr Weltraum-Imperium auf.'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: 'Solarkraftwerk bauen',
|
||||||
|
content:
|
||||||
|
'Bauen Sie zuerst ein Solarkraftwerk! Es liefert Energie für Ihren Planeten. Ohne Energie können andere Ressourcengebäude nicht funktionieren. Dies ist der wichtigste erste Schritt.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Bauauftrag',
|
||||||
|
content:
|
||||||
|
'Ihr Gebäude befindet sich jetzt in der Bauauftragsliste. Klicken Sie auf das Warteschlangensymbol oben rechts, um alle laufenden Bau- und Forschungsaufträge anzuzeigen. Gebäude brauchen Zeit zum Fertigstellen, aber Sie können während des Wartens weitermachen.'
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: 'Willkommen bei OGame (Mobil)',
|
||||||
|
content:
|
||||||
|
'Willkommen, Kommandant! Dies ist ein optimiertes Tutorial für Touchscreens. Wir werden schnell die Kernfunktionen durchgehen, damit Sie mit dem Aufbau Ihres Imperiums beginnen können.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Bauauftrag',
|
||||||
|
content:
|
||||||
|
'Klicken Sie auf das Warteschlangensymbol oben rechts, um den Baufortschritt anzuzeigen. Sie können weiter andere Seiten durchsuchen - der Bau läuft im Hintergrund.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: 'Schließen',
|
||||||
|
gotIt: 'Verstanden',
|
||||||
|
dontShowAgain: 'Nicht mehr anzeigen',
|
||||||
|
resetHints: 'Hinweise zurücksetzen',
|
||||||
|
resetHintsDesc: 'Alle Hinweise wieder anzeigen',
|
||||||
|
hintsEnabled: 'Hinweise aktivieren',
|
||||||
|
hintsEnabledDesc: 'Hilfreiche Hinweise beim Seitenbesuch anzeigen',
|
||||||
|
overview: {
|
||||||
|
title: 'Planetenübersicht',
|
||||||
|
message:
|
||||||
|
'Hier sehen Sie Planetenressourcen, Flottenstatus und Produktionsdetails. Schauen Sie regelmäßig vorbei, um Ihren Fortschritt zu überwachen!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: 'Gebäude',
|
||||||
|
message:
|
||||||
|
'Bauen und verbessern Sie Strukturen hier. Beginnen Sie mit dem Solarkraftwerk für Energie, dann Ressourcenminen. Tipp: Roboterfabrik beschleunigt den Bau!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: 'Forschungslabor',
|
||||||
|
message:
|
||||||
|
'Erforschen Sie Technologien, um neue Schiffe freizuschalten, Kampfkraft zu verbessern und Ihre Zivilisation voranzubringen. Energietechnik ist ein guter Anfang!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: 'Raumschiffswerft',
|
||||||
|
message:
|
||||||
|
'Bauen Sie Schiffe zum Erkunden, Transportieren von Ressourcen und Verteidigen Ihres Imperiums. Frachter helfen beim Transport zwischen Planeten.'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: 'Flottenkommando',
|
||||||
|
message:
|
||||||
|
'Senden Sie Ihre Schiffe auf Missionen: Feinde angreifen, Ressourcen transportieren, neue Planeten besiedeln oder Trümmerfelder erkunden.'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: 'Galaxiekarte',
|
||||||
|
message:
|
||||||
|
'Erkunden Sie die Galaxie, um leere Planeten zum Besiedeln, Trümmerfelder zum Ernten und Feinde zum Angreifen zu finden. Nutzen Sie zuerst Spionagesonden!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: 'Diplomatie',
|
||||||
|
message:
|
||||||
|
'Verwalten Sie Beziehungen mit NPCs. Senden Sie Geschenke, um den Ruf zu verbessern, oder stellen Sie sich feindlichen Angriffen. Verbündete Ihrer Feinde könnten auch feindlich werden!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: 'Nachrichten',
|
||||||
|
message:
|
||||||
|
'Sehen Sie hier Kampfberichte, Spionageberichte und diplomatische Benachrichtigungen. Verfolgen Sie Ihre Aktivitäten und Feindkontakte.'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: 'Planetenverteidigung',
|
||||||
|
message:
|
||||||
|
'Bauen Sie Verteidigungsstrukturen, um Ihren Planeten vor Angriffen zu schützen. Schilde und Geschütze können Angreifer abschrecken!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: 'Offiziere',
|
||||||
|
message:
|
||||||
|
'Rekrutieren Sie Offiziere für verschiedene Boni! Kommandant beschleunigt den Bau, Geologe steigert Ressourcenproduktion, Admiral verbessert Flottenkapazitäten.'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: 'Kampfsimulator',
|
||||||
|
message:
|
||||||
|
'Simulieren Sie Kampfergebnisse vor dem Angriff. Geben Sie Flotten und Technologiestufen ein, um Sieg, Verluste und Beute vorherzusagen.'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Einstellungen',
|
||||||
|
message: 'Verwalten Sie hier Spieldaten, Benachrichtigungen und Import/Export. Sichern Sie regelmäßig Ihren Fortschritt!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM-Panel',
|
||||||
|
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.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: 'Conquer the Stars',
|
||||||
|
startGame: 'Start Game',
|
||||||
|
privacyAgreement: 'Privacy Agreement',
|
||||||
|
privacyAgreementDesc: 'Please read and agree to our privacy policy before starting the game.',
|
||||||
|
agreeToPrivacy: 'I have read and agree to',
|
||||||
|
viewFullPolicy: 'View Full Policy',
|
||||||
|
agreeAndStart: 'Agree & Start'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
@@ -31,10 +40,12 @@ export default {
|
|||||||
goToBuildings: 'Go to Buildings',
|
goToBuildings: 'Go to Buildings',
|
||||||
locked: 'Locked',
|
locked: 'Locked',
|
||||||
viewRequirements: 'View Requirements',
|
viewRequirements: 'View Requirements',
|
||||||
|
requirements: 'Requirements',
|
||||||
requirementsNotMet: 'Requirements Not Met',
|
requirementsNotMet: 'Requirements Not Met',
|
||||||
current: 'Current',
|
current: 'Current',
|
||||||
level: 'Level',
|
level: 'Level',
|
||||||
gmModeActivated: 'GM Mode Activated! Check the navigation menu.'
|
gmModeActivated: 'GM Mode Activated! Check the navigation menu.',
|
||||||
|
view: 'View'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: 'Requirements not met',
|
requirementsNotMet: 'Requirements not met',
|
||||||
@@ -56,7 +67,8 @@ export default {
|
|||||||
buildingLevelZero: 'Building level is 0, cannot demolish',
|
buildingLevelZero: 'Building level is 0, cannot demolish',
|
||||||
researchQueueFull: 'Research queue full',
|
researchQueueFull: 'Research queue full',
|
||||||
moonExists: 'Moon already exists',
|
moonExists: 'Moon already exists',
|
||||||
insufficientDebris: 'Insufficient debris field'
|
insufficientDebris: 'Insufficient debris field',
|
||||||
|
launchFailed: 'Launch failed'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
@@ -111,7 +123,13 @@ export default {
|
|||||||
homePlanet: 'Home Planet',
|
homePlanet: 'Home Planet',
|
||||||
planetPrefix: 'Planet',
|
planetPrefix: 'Planet',
|
||||||
moonSuffix: "'s Moon",
|
moonSuffix: "'s Moon",
|
||||||
colonyPrefix: 'Colony'
|
colonyPrefix: 'Colony',
|
||||||
|
renamePlanet: 'Rename Planet',
|
||||||
|
renamePlanetTitle: 'Rename Planet',
|
||||||
|
newPlanetName: 'New Name',
|
||||||
|
planetNamePlaceholder: 'Enter new planet name',
|
||||||
|
rename: 'Rename',
|
||||||
|
renameSuccess: 'Planet renamed to {name}'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: 'Total Points'
|
points: 'Total Points'
|
||||||
@@ -125,6 +143,7 @@ export default {
|
|||||||
roboticsFactory: 'Robotics Factory',
|
roboticsFactory: 'Robotics Factory',
|
||||||
naniteFactory: 'Nanite Factory',
|
naniteFactory: 'Nanite Factory',
|
||||||
shipyard: 'Shipyard',
|
shipyard: 'Shipyard',
|
||||||
|
hangar: 'Hangar',
|
||||||
researchLab: 'Research Lab',
|
researchLab: 'Research Lab',
|
||||||
metalStorage: 'Metal Storage',
|
metalStorage: 'Metal Storage',
|
||||||
crystalStorage: 'Crystal Storage',
|
crystalStorage: 'Crystal Storage',
|
||||||
@@ -163,6 +182,7 @@ export default {
|
|||||||
roboticsFactory: 'Accelerates construction speed',
|
roboticsFactory: 'Accelerates construction speed',
|
||||||
naniteFactory: 'Increases build queue capacity, +1 per level (max 10 levels)',
|
naniteFactory: 'Increases build queue capacity, +1 per level (max 10 levels)',
|
||||||
shipyard: 'Constructs ships',
|
shipyard: 'Constructs ships',
|
||||||
|
hangar: 'Specialized facility for expanding fleet storage capacity, supports planetary specialization',
|
||||||
researchLab: 'Researches technologies',
|
researchLab: 'Researches technologies',
|
||||||
metalStorage: 'Increases metal storage capacity',
|
metalStorage: 'Increases metal storage capacity',
|
||||||
crystalStorage: 'Increases crystal storage capacity',
|
crystalStorage: 'Increases crystal storage capacity',
|
||||||
@@ -197,10 +217,10 @@ export default {
|
|||||||
lightFighter: 'Basic combat unit',
|
lightFighter: 'Basic combat unit',
|
||||||
heavyFighter: 'Heavily armored fighter',
|
heavyFighter: 'Heavily armored fighter',
|
||||||
cruiser: 'Medium warship, balanced offense and defense',
|
cruiser: 'Medium warship, balanced offense and defense',
|
||||||
battleship: 'Powerful warship',
|
battleship: 'Main heavy warship with powerful firepower and strong defense',
|
||||||
battlecruiser: 'Fast powerful warship, excels at attacking battleships',
|
battlecruiser: 'Fast powerful warship, excels at attacking battleships',
|
||||||
bomber: 'Specialized ship for attacking defense structures',
|
bomber: 'Specialized ship for attacking defense structures',
|
||||||
destroyer: 'Hunter specialized in destroying large ships',
|
destroyer: 'Specialized anti-capital ship with high firepower but low defense',
|
||||||
smallCargo: 'Transports small amounts of resources',
|
smallCargo: 'Transports small amounts of resources',
|
||||||
largeCargo: 'Transports large amounts of resources',
|
largeCargo: 'Transports large amounts of resources',
|
||||||
colonyShip: 'Used to colonize new planets',
|
colonyShip: 'Used to colonize new planets',
|
||||||
@@ -278,8 +298,9 @@ export default {
|
|||||||
ionTechnology: 'Ion weapon technology',
|
ionTechnology: 'Ion weapon technology',
|
||||||
hyperspaceTechnology: 'Hyperspace jump technology',
|
hyperspaceTechnology: 'Hyperspace jump technology',
|
||||||
plasmaTechnology: 'Plasma weapon technology',
|
plasmaTechnology: 'Plasma weapon technology',
|
||||||
computerTechnology: 'Increases research queue capacity, +1 per level (max 10 levels)',
|
computerTechnology: 'Increases research queue and fleet mission slots, +1 queue +1 slot per level (max 10 levels)',
|
||||||
espionageTechnology: 'Improves spy probe effectiveness, +1 espionage level per level',
|
espionageTechnology:
|
||||||
|
'Improves spy probe effectiveness, +1 espionage level per level. Spy level = your level - enemy level + probes/5. ≥-1 shows fleet, ≥1 shows defense, ≥3 shows buildings, ≥5 shows technologies',
|
||||||
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
weaponsTechnology: 'Increases ship and defense attack power by 10% per level',
|
||||||
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
shieldingTechnology: 'Increases ship and defense shields by 10% per level',
|
||||||
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
armourTechnology: 'Increases ship and defense armour by 10% per level',
|
||||||
@@ -312,6 +333,8 @@ export default {
|
|||||||
darkMatterSpecialist: 'Improves dark matter collection efficiency'
|
darkMatterSpecialist: 'Improves dark matter collection efficiency'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: 'Active Tasks',
|
||||||
|
empty: 'No active queues',
|
||||||
buildQueueBonus: 'Build Queue',
|
buildQueueBonus: 'Build Queue',
|
||||||
spaceBonus: 'Space Bonus',
|
spaceBonus: 'Space Bonus',
|
||||||
buildSpeedBonus: 'Build Speed Bonus',
|
buildSpeedBonus: 'Build Speed Bonus',
|
||||||
@@ -319,6 +342,7 @@ export default {
|
|||||||
researchQueueBonus: 'Research Queue',
|
researchQueueBonus: 'Research Queue',
|
||||||
building: 'Building',
|
building: 'Building',
|
||||||
researching: 'Researching',
|
researching: 'Researching',
|
||||||
|
demolishing: 'Demolishing',
|
||||||
remaining: 'Remaining',
|
remaining: 'Remaining',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
cancelBuild: 'Cancel Build',
|
cancelBuild: 'Cancel Build',
|
||||||
@@ -326,7 +350,14 @@ export default {
|
|||||||
confirmCancel: 'Are you sure you want to cancel? 50% of resources will be refunded.',
|
confirmCancel: 'Are you sure you want to cancel? 50% of resources will be refunded.',
|
||||||
level: 'Level',
|
level: 'Level',
|
||||||
quantity: 'Quantity',
|
quantity: 'Quantity',
|
||||||
upgradeToLevel: 'Upgrade to Level'
|
upgradeToLevel: 'Upgrade to Level',
|
||||||
|
tabs: {
|
||||||
|
all: 'All',
|
||||||
|
buildings: 'Buildings',
|
||||||
|
research: 'Research',
|
||||||
|
ships: 'Ships',
|
||||||
|
defense: 'Defense'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: 'Planet Overview',
|
title: 'Planet Overview',
|
||||||
@@ -348,6 +379,7 @@ export default {
|
|||||||
level: 'Level',
|
level: 'Level',
|
||||||
upgradeCost: 'Upgrade Cost',
|
upgradeCost: 'Upgrade Cost',
|
||||||
buildTime: 'Build Time',
|
buildTime: 'Build Time',
|
||||||
|
build: 'Build',
|
||||||
upgrade: 'Upgrade',
|
upgrade: 'Upgrade',
|
||||||
maxLevelReached: 'Max Level Reached',
|
maxLevelReached: 'Max Level Reached',
|
||||||
requirementsNotMet: 'Requirements Not Met',
|
requirementsNotMet: 'Requirements Not Met',
|
||||||
@@ -462,6 +494,7 @@ export default {
|
|||||||
colonize: 'Colonize',
|
colonize: 'Colonize',
|
||||||
spy: 'Spy',
|
spy: 'Spy',
|
||||||
deploy: 'Deploy',
|
deploy: 'Deploy',
|
||||||
|
expedition: 'Expedition',
|
||||||
recycle: 'Recycle',
|
recycle: 'Recycle',
|
||||||
destroy: 'Planet Destruction',
|
destroy: 'Planet Destruction',
|
||||||
transportResources: 'Transport Resources',
|
transportResources: 'Transport Resources',
|
||||||
@@ -477,7 +510,8 @@ export default {
|
|||||||
recallFleet: 'Recall Fleet',
|
recallFleet: 'Recall Fleet',
|
||||||
abortMission: 'Abort Mission',
|
abortMission: 'Abort Mission',
|
||||||
abortMissionTitle: 'Confirm Abort Mission',
|
abortMissionTitle: 'Confirm Abort Mission',
|
||||||
abortMissionWarning: 'WARNING: Aborting this mission will permanently lose {ships} ships and {resources} resources!\n\nThis action is irreversible and the fleet and resources will not return.',
|
abortMissionWarning:
|
||||||
|
'WARNING: Aborting this mission will permanently lose {ships} ships and {resources} resources!\n\nThis action is irreversible and the fleet and resources will not return.',
|
||||||
abortMissionSuccess: 'Mission Aborted',
|
abortMissionSuccess: 'Mission Aborted',
|
||||||
abortMissionSuccessMessage: 'Mission has been aborted, fleet and resources are lost.',
|
abortMissionSuccessMessage: 'Mission has been aborted, fleet and resources are lost.',
|
||||||
sendFailed: 'Send Failed',
|
sendFailed: 'Send Failed',
|
||||||
@@ -581,17 +615,20 @@ export default {
|
|||||||
debris: 'Debris',
|
debris: 'Debris',
|
||||||
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"
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: 'Messages',
|
title: 'Messages',
|
||||||
battles: 'Battles',
|
battles: 'Battles',
|
||||||
spy: 'Spy',
|
spy: 'Spy',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: 'Diplomacy',
|
||||||
battleReports: 'Battle Reports',
|
battleReports: 'Battle Reports',
|
||||||
spyReports: 'Spy Reports',
|
spyReports: 'Spy Reports',
|
||||||
noBattleReports: 'No battle reports',
|
noBattleReports: 'No battle reports',
|
||||||
noSpyReports: 'No spy reports',
|
noSpyReports: 'No spy reports',
|
||||||
|
noDiplomaticReports: 'No diplomatic reports',
|
||||||
battleReport: 'Battle Report',
|
battleReport: 'Battle Report',
|
||||||
spyReport: 'Spy Report',
|
spyReport: 'Spy Report',
|
||||||
victory: 'Victory',
|
victory: 'Victory',
|
||||||
@@ -611,6 +648,9 @@ export default {
|
|||||||
buildings: 'Buildings',
|
buildings: 'Buildings',
|
||||||
unread: 'Unread',
|
unread: 'Unread',
|
||||||
targetPlanet: 'Target Planet',
|
targetPlanet: 'Target Planet',
|
||||||
|
attackerRemaining: 'Attacker Remaining',
|
||||||
|
defenderRemaining: 'Defender Remaining',
|
||||||
|
allDestroyed: 'All destroyed',
|
||||||
spied: 'Spied',
|
spied: 'Spied',
|
||||||
spiedNotification: 'Spied Notification',
|
spiedNotification: 'Spied Notification',
|
||||||
noSpiedNotifications: 'No spied notifications',
|
noSpiedNotifications: 'No spied notifications',
|
||||||
@@ -639,7 +679,49 @@ export default {
|
|||||||
hostile: 'They are hostile towards you and do not accept gifts',
|
hostile: 'They are hostile towards you and do not accept gifts',
|
||||||
neutral_distrust: 'They lack trust in you',
|
neutral_distrust: 'They lack trust in you',
|
||||||
polite_decline: 'They politely declined'
|
polite_decline: 'They politely declined'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: 'Spied Notification Details',
|
||||||
|
spyDetected: 'Spy Detected',
|
||||||
|
detectionResult: 'Detection Result',
|
||||||
|
detectionSuccess: 'You detected the enemy spy!',
|
||||||
|
spiedNotificationMessage: '{npc} attempted to spy on your planet {planet}',
|
||||||
|
spiedNotificationTip: 'Consider increasing your defense or counter-attacking if this NPC is hostile',
|
||||||
|
viewInGalaxy: 'View in Galaxy',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: 'Mission Report Details',
|
||||||
|
missionSuccess: 'Success',
|
||||||
|
missionFailed: 'Failed',
|
||||||
|
origin: 'Origin',
|
||||||
|
destination: 'Destination',
|
||||||
|
missionDetails: 'Mission Details',
|
||||||
|
transportedResources: 'Transported Resources',
|
||||||
|
recycledResources: 'Recycled Resources',
|
||||||
|
remainingDebris: 'Remaining Debris',
|
||||||
|
newPlanet: 'New Planet',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: 'NPC Activity Details',
|
||||||
|
activityType: {
|
||||||
|
recycle: 'Recycling Debris'
|
||||||
|
},
|
||||||
|
activityLocation: 'Activity Location',
|
||||||
|
position: 'Position',
|
||||||
|
nearPlanet: 'Near Planet',
|
||||||
|
activityDescription: 'Activity Description',
|
||||||
|
npcActivityMessage: '{npc} is {activity} at {position}',
|
||||||
|
arrivalTime: 'Arrival Time',
|
||||||
|
npcActivityTip: 'NPCs may collect debris from battles. You can try to reach the location first if you want to compete for resources',
|
||||||
|
// Clear messages
|
||||||
|
clearMessages: 'Clear Messages',
|
||||||
|
clearMessageTypes: 'Select message types to clear',
|
||||||
|
clearBattleReports: 'Battle Reports',
|
||||||
|
clearSpyReports: 'Spy Reports',
|
||||||
|
clearSpiedNotifications: 'Spied Notifications',
|
||||||
|
clearMissionReports: 'Mission Reports',
|
||||||
|
clearNPCActivity: 'NPC Activity',
|
||||||
|
clearGiftNotifications: 'Gift Notifications',
|
||||||
|
clearGiftRejected: 'Rejected Gifts',
|
||||||
|
clearNow: 'Clear Now'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: 'Transport mission completed successfully',
|
transportSuccess: 'Transport mission completed successfully',
|
||||||
@@ -651,7 +733,19 @@ export default {
|
|||||||
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',
|
||||||
destroySuccess: 'Planet destruction mission executed successfully',
|
destroySuccess: 'Planet destruction mission executed successfully',
|
||||||
destroyFailed: 'Planet destruction mission failed'
|
destroyFailed: 'Planet destruction mission failed',
|
||||||
|
missileAttackSuccess: 'Missile attack successful',
|
||||||
|
missileAttackFailed: 'Missile attack failed, target planet does not exist',
|
||||||
|
missileAttackIntercepted: 'All missiles intercepted',
|
||||||
|
hits: 'hits',
|
||||||
|
expeditionResources: 'The expedition found resources!',
|
||||||
|
expeditionDarkMatter: 'The expedition found dark matter!',
|
||||||
|
expeditionFleet: 'The expedition found abandoned ships!',
|
||||||
|
expeditionPiratesAttack: 'The expedition was attacked by pirates and lost some ships',
|
||||||
|
expeditionPiratesEscaped: 'The expedition encountered pirates but escaped successfully',
|
||||||
|
expeditionAliensAttack: 'The expedition was attacked by aliens and lost some ships',
|
||||||
|
expeditionAliensEscaped: 'The expedition encountered aliens but escaped successfully',
|
||||||
|
expeditionNothing: 'The expedition found nothing'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: 'Battle Simulator',
|
title: 'Battle Simulator',
|
||||||
@@ -746,7 +840,31 @@ export default {
|
|||||||
buildDate: 'Build Date',
|
buildDate: 'Build Date',
|
||||||
community: 'Community',
|
community: 'Community',
|
||||||
github: 'GitHub Repository',
|
github: 'GitHub Repository',
|
||||||
qqGroup: 'QQ Group'
|
qqGroup: 'QQ Group',
|
||||||
|
privacyPolicy: 'Privacy Policy',
|
||||||
|
displaySettings: 'Display Settings',
|
||||||
|
displaySettingsDesc: 'Adjust visual effects',
|
||||||
|
backgroundAnimation: 'Background Animation',
|
||||||
|
backgroundAnimationDesc: 'Show starfield/particle background animation (may affect performance)',
|
||||||
|
notifications: 'Notification Settings',
|
||||||
|
notificationsDesc: 'Manage in-game notification alerts',
|
||||||
|
notificationTypes: 'Notification Types',
|
||||||
|
browserNotifications: 'Browser Notifications',
|
||||||
|
inAppNotifications: 'In-App Notifications',
|
||||||
|
constructionComplete: 'Construction Complete',
|
||||||
|
researchComplete: 'Research Complete',
|
||||||
|
browserPermission: 'Enable Browser Notifications',
|
||||||
|
permissionGranted: 'Permission Granted',
|
||||||
|
permissionDenied: 'Permission Denied/Not Granted',
|
||||||
|
inAppNotificationsDesc: 'Show via in-page popups',
|
||||||
|
notificationsDisabled: 'Enable any switch above to configure specific notifications',
|
||||||
|
suppressInFocus: 'Suppress browser notifications when page is focused',
|
||||||
|
expandTypes: 'Expand Details',
|
||||||
|
collapseTypes: 'Collapse Details'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: 'Construction Complete',
|
||||||
|
researchComplete: 'Research Complete'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM Control Panel',
|
title: 'GM Control Panel',
|
||||||
@@ -798,7 +916,8 @@ export default {
|
|||||||
npcWillSpyAndAttack: '{npcName} will spy in 5s and attack in 10s',
|
npcWillSpyAndAttack: '{npcName} will spy in 5s and attack in 10s',
|
||||||
acceleratedMissions: 'Accelerated {count} missions to 5 seconds',
|
acceleratedMissions: 'Accelerated {count} missions to 5 seconds',
|
||||||
npcFleetInitialized: '{npcName} fleet initialized',
|
npcFleetInitialized: '{npcName} fleet initialized',
|
||||||
npcFleetDetails: '100 Spy Probes\n500 Light Fighters\n300 Heavy Fighters\n200 Cruisers\n100 Battleships\n50 Bombers\n30 Destroyers\n20 Battlecruisers',
|
npcFleetDetails:
|
||||||
|
'100 Spy Probes\n500 Light Fighters\n300 Heavy Fighters\n200 Cruisers\n100 Battleships\n50 Bombers\n30 Destroyers\n20 Battlecruisers',
|
||||||
dangerZone: 'Danger Zone',
|
dangerZone: 'Danger Zone',
|
||||||
dangerZoneDesc: 'The following operations are irreversible',
|
dangerZoneDesc: 'The following operations are irreversible',
|
||||||
resetGame: 'Reset Game',
|
resetGame: 'Reset Game',
|
||||||
@@ -806,9 +925,11 @@ export default {
|
|||||||
completeAllQueues: 'Complete All Queues',
|
completeAllQueues: 'Complete All Queues',
|
||||||
completeAllQueuesDesc: 'Instantly complete all building, research, ship, defense queues and fleet missions',
|
completeAllQueuesDesc: 'Instantly complete all building, research, ship, defense queues and fleet missions',
|
||||||
completeQueues: 'Complete Queues',
|
completeQueues: 'Complete Queues',
|
||||||
completeQueuesSuccess: 'Completed {buildingCount} building queues, {researchCount} research queues, {missionCount} fleet missions, {missileCount} missile attacks'
|
completeQueuesSuccess:
|
||||||
|
'Completed {buildingCount} building queues, {researchCount} research queues, {missionCount} fleet missions, {missileCount} missile attacks'
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count} Incoming Enemy Fleets',
|
||||||
npcSpyIncoming: 'NPC Spy Probe Incoming',
|
npcSpyIncoming: 'NPC Spy Probe Incoming',
|
||||||
npcAttackIncoming: 'NPC Fleet Attack Incoming!',
|
npcAttackIncoming: 'NPC Fleet Attack Incoming!',
|
||||||
npcFleetIncoming: 'NPC Fleet Approaching',
|
npcFleetIncoming: 'NPC Fleet Approaching',
|
||||||
@@ -820,6 +941,30 @@ export default {
|
|||||||
npcSpiedYourPlanet: 'NPC spied your planet',
|
npcSpiedYourPlanet: 'NPC spied your planet',
|
||||||
npcAttackedYourPlanet: 'NPC attacked your planet'
|
npcAttackedYourPlanet: 'NPC attacked your planet'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: 'Enemy Alert',
|
||||||
|
markAllRead: 'Mark All Read',
|
||||||
|
noAlerts: 'No alerts',
|
||||||
|
fleetSize: 'Fleet Size',
|
||||||
|
ships: 'ships',
|
||||||
|
viewFleet: 'View Fleet',
|
||||||
|
alertDetails: 'Alert Details',
|
||||||
|
targetInfo: 'Target Info',
|
||||||
|
arrivalTime: 'Arrival Time',
|
||||||
|
countdown: 'Countdown',
|
||||||
|
viewMessages: 'View Messages',
|
||||||
|
arrived: 'Arrived',
|
||||||
|
missionType: {
|
||||||
|
spy: 'Spy',
|
||||||
|
attack: 'Attack',
|
||||||
|
unknown: 'Unknown'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: 'Enemy spy incoming!',
|
||||||
|
attack: 'Enemy attack incoming!',
|
||||||
|
unknown: 'Enemy fleet incoming!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: 'Diplomacy',
|
title: 'Diplomacy',
|
||||||
description: 'Manage diplomatic relations with NPCs',
|
description: 'Manage diplomatic relations with NPCs',
|
||||||
@@ -836,6 +981,10 @@ export default {
|
|||||||
recentEvents: 'Recent Events',
|
recentEvents: 'Recent Events',
|
||||||
recentEventsDescription: 'Recent diplomatic activity log',
|
recentEventsDescription: 'Recent diplomatic activity log',
|
||||||
ago: 'ago',
|
ago: 'ago',
|
||||||
|
notifications: 'Diplomatic Notifications',
|
||||||
|
markAllRead: 'Mark All Read',
|
||||||
|
noReports: 'No diplomatic events',
|
||||||
|
viewAll: 'View All',
|
||||||
status: {
|
status: {
|
||||||
friendly: 'Friendly',
|
friendly: 'Friendly',
|
||||||
neutral: 'Neutral',
|
neutral: 'Neutral',
|
||||||
@@ -848,9 +997,30 @@ export default {
|
|||||||
more: 'more',
|
more: 'more',
|
||||||
actions: {
|
actions: {
|
||||||
gift: 'Send Gift',
|
gift: 'Send Gift',
|
||||||
viewPlanets: 'View Planets'
|
viewPlanets: 'View Planets',
|
||||||
|
addNote: 'Add Note',
|
||||||
|
editNote: 'Edit Note'
|
||||||
},
|
},
|
||||||
|
note: 'Note',
|
||||||
|
notePlaceholder: 'Enter note...',
|
||||||
|
noteEmpty: 'No note',
|
||||||
lastEvent: 'Last Event',
|
lastEvent: 'Last Event',
|
||||||
|
reportDetails: 'Diplomatic Report Details',
|
||||||
|
eventDescription: 'Event Description',
|
||||||
|
reputationChange: 'Reputation Change',
|
||||||
|
before: 'Before',
|
||||||
|
after: 'After',
|
||||||
|
statusChange: 'Status Change',
|
||||||
|
viewDiplomacy: 'View Diplomacy Page',
|
||||||
|
eventType: {
|
||||||
|
gift: 'Sent resources',
|
||||||
|
attack: 'Launched an attack',
|
||||||
|
allyAttacked: 'Attacked an ally',
|
||||||
|
spy: 'Conducted espionage',
|
||||||
|
stealDebris: 'Stole debris',
|
||||||
|
destroyPlanet: 'Destroyed a planet',
|
||||||
|
unknown: 'Unknown event'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: 'Sent Gift',
|
gift: 'Sent Gift',
|
||||||
attack: 'Attack',
|
attack: 'Attack',
|
||||||
@@ -863,7 +1033,7 @@ export default {
|
|||||||
giftedResources: 'Gifted {metal}M {crystal}C {deuterium}D',
|
giftedResources: 'Gifted {metal}M {crystal}C {deuterium}D',
|
||||||
receivedGiftFromPlayer: 'Received gift from player',
|
receivedGiftFromPlayer: 'Received gift from player',
|
||||||
giftedToNpc: 'You gifted resources to {npcName}. Reputation +{reputation}',
|
giftedToNpc: 'You gifted resources to {npcName}. Reputation +{reputation}',
|
||||||
rejectedPlayerGift: 'Rejected player\'s gift',
|
rejectedPlayerGift: "Rejected player's gift",
|
||||||
npcRejectedGift: '{npcName} rejected your gift. Reputation {reputation}',
|
npcRejectedGift: '{npcName} rejected your gift. Reputation {reputation}',
|
||||||
attackedNpc: 'Attacked {npcName}',
|
attackedNpc: 'Attacked {npcName}',
|
||||||
wasAttackedByPlayer: 'Was attacked by player',
|
wasAttackedByPlayer: 'Was attacked by player',
|
||||||
@@ -872,14 +1042,65 @@ export default {
|
|||||||
allyDispleased: '{allyName} is displeased that you attacked their ally {targetName}',
|
allyDispleased: '{allyName} is displeased that you attacked their ally {targetName}',
|
||||||
wasSpiedByPlayer: 'Was spied by player (detected: {detected})',
|
wasSpiedByPlayer: 'Was spied by player (detected: {detected})',
|
||||||
spyDetected: 'Your espionage was detected by {npcName}',
|
spyDetected: 'Your espionage was detected by {npcName}',
|
||||||
stoleDebrisFromTerritory: 'Stole debris from {npcName}\'s territory',
|
stoleDebrisFromTerritory: "Stole debris from {npcName}'s territory",
|
||||||
playerStoleDebris: 'Player stole debris from territory',
|
playerStoleDebris: 'Player stole debris from territory',
|
||||||
recycledDebrisNearNpc: 'You recycled debris near {npcName}\'s planet. They are displeased.',
|
recycledDebrisNearNpc: "You recycled debris near {npcName}'s planet. They are displeased.",
|
||||||
giftedResourcesToPlayer: 'Gifted resources to player',
|
giftedResourcesToPlayer: 'Gifted resources to player',
|
||||||
receivedGiftFromNpc: 'Received gift from {npcName}',
|
receivedGiftFromNpc: 'Received gift from {npcName}',
|
||||||
acceptedGiftFromNpc: 'You accepted a gift from {npcName}: {metal}M {crystal}C {deuterium}D',
|
acceptedGiftFromNpc: 'You accepted a gift from {npcName}: {metal}M {crystal}C {deuterium}D',
|
||||||
playerRejectedGift: 'Player rejected gift',
|
playerRejectedGift: 'Player rejected gift',
|
||||||
rejectedGiftFromNpc: 'You rejected a gift from {npcName}. Reputation {reputation}'
|
rejectedGiftFromNpc: 'You rejected a gift from {npcName}. Reputation {reputation}',
|
||||||
|
destroyedNpcPlanet: "Destroyed {npcName}'s {planetName}",
|
||||||
|
playerDestroyedPlanet: 'Player destroyed {planetName}',
|
||||||
|
youDestroyedNpcPlanet: "You destroyed {npcName}'s {planetName}. Reputation {reputation}",
|
||||||
|
playerDestroyedAllyPlanet: "Player destroyed ally {allyName}'s {planetName}",
|
||||||
|
allyOutraged: "{allyName} is outraged that you destroyed their ally {targetName}'s {planetName}",
|
||||||
|
npcEliminated: 'NPC {npcName} has been completely eliminated',
|
||||||
|
npcEliminatedMessage: "You destroyed all of {npcName}'s planets! This faction has been completely wiped out."
|
||||||
|
},
|
||||||
|
searchPlaceholder: 'Search NPC name...',
|
||||||
|
viewMode: {
|
||||||
|
card: 'Card',
|
||||||
|
list: 'List'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC Diagnostic',
|
||||||
|
title: 'NPC Status Diagnostic',
|
||||||
|
description:
|
||||||
|
'Player points: {points}, Spy interval: {spyInterval}min, Attack interval: {attackInterval}min, Attack probability: {attackProb}%',
|
||||||
|
noData: 'No NPC data',
|
||||||
|
difficulty: 'Difficulty',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: 'Easy',
|
||||||
|
medium: 'Medium',
|
||||||
|
hard: 'Hard'
|
||||||
|
},
|
||||||
|
reputation: 'Reputation',
|
||||||
|
spyProbes: 'Spy Probes',
|
||||||
|
fleetPower: 'Fleet Power',
|
||||||
|
canSpy: 'Can Spy',
|
||||||
|
canAttack: 'Can Attack',
|
||||||
|
attackProbability: 'Attack Probability',
|
||||||
|
nextSpy: 'Next Spy',
|
||||||
|
nextAttack: 'Next Attack',
|
||||||
|
yes: 'Yes',
|
||||||
|
no: 'No',
|
||||||
|
timeFormat: '{min}m {sec}s',
|
||||||
|
anytime: 'Anytime',
|
||||||
|
statusExplanation: 'Status Explanation',
|
||||||
|
noRelation: 'No Relation',
|
||||||
|
noRelationNeutral: 'No Relation (Neutral)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: 'Friendly relationship, will not act',
|
||||||
|
neutralNoAction: 'Neutral relationship, will not act',
|
||||||
|
hostileWillAct: 'Hostile relationship, may take action',
|
||||||
|
noRelationNeutral: 'No diplomatic relation, treated as neutral',
|
||||||
|
insufficientProbes: 'Insufficient probes (Current: {current}, Required: {required})',
|
||||||
|
noFleet: 'No combat fleet',
|
||||||
|
spyCooldown: 'Spy on cooldown ({min}m {sec}s)',
|
||||||
|
attackCooldown: 'Attack on cooldown ({min}m {sec}s)',
|
||||||
|
notSpiedYet: 'Not yet spied, need to spy first'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -893,5 +1114,276 @@ export default {
|
|||||||
title: 'Page Not Found',
|
title: 'Page Not Found',
|
||||||
description: 'Sorry, the page you are looking for does not exist',
|
description: 'Sorry, the page you are looking for does not exist',
|
||||||
goHome: 'Go Home'
|
goHome: 'Go Home'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: 'Introduction',
|
||||||
|
content:
|
||||||
|
'This privacy policy explains how OGame-Vue-Ts handles your data. We are committed to protecting your privacy, and this game is designed with complete respect for user privacy.'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: 'Data Collection',
|
||||||
|
content: 'This game only collects and stores the following data locally in your browser:',
|
||||||
|
items: {
|
||||||
|
gameProgress: 'Game progress (building levels, fleets, resources, etc.)',
|
||||||
|
settings: 'Game settings (notification preferences, display options, etc.)',
|
||||||
|
language: 'Language preference'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: 'Data Storage',
|
||||||
|
content:
|
||||||
|
"All data is stored in your browser's local storage (localStorage). This means your data always remains on your own device, and we cannot access, view, or collect any of your game data."
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: 'No Server Communication',
|
||||||
|
content:
|
||||||
|
'This game is a completely offline single-player game. Except for the update check feature (which fetches version information from GitHub), the game does not communicate with any server. Your game data never leaves your device.'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: 'Third-Party Services',
|
||||||
|
content:
|
||||||
|
'This game uses third-party analytics services to track visitor statistics and traffic sources, helping us understand usage patterns and improve the gaming experience. This analytics data is anonymous and does not contain any personally identifiable information. We do not use any advertising services or other commercial tracking tools.'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: 'Data Control',
|
||||||
|
content: 'You have complete control over your data:',
|
||||||
|
items: {
|
||||||
|
export: 'You can export your game data at any time',
|
||||||
|
import: 'You can import data from backup files',
|
||||||
|
delete: 'You can delete all data by clearing browser data or using the in-game "Clear Data" feature'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: 'Contact Us',
|
||||||
|
content: 'If you have any questions about this privacy policy, please contact us via:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: 'days',
|
||||||
|
hours: 'hours',
|
||||||
|
minutes: 'minutes',
|
||||||
|
seconds: 'seconds'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
progress: 'Progress',
|
||||||
|
previous: 'Previous',
|
||||||
|
next: 'Next',
|
||||||
|
gotIt: 'Got It',
|
||||||
|
completeButton: 'Complete',
|
||||||
|
skip: 'Skip Tutorial',
|
||||||
|
welcome: {
|
||||||
|
title: 'Welcome to OGame',
|
||||||
|
content:
|
||||||
|
'Welcome, Commander! This tutorial will guide you through the basics of building your empire. Click "Next" to begin your journey.'
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
title: 'Resource Overview',
|
||||||
|
content:
|
||||||
|
'These are your resources: Metal, Crystal, and Deuterium. They are essential for building structures and researching technologies. Energy is also important to power your infrastructure.'
|
||||||
|
},
|
||||||
|
planet: {
|
||||||
|
title: 'Your Planet',
|
||||||
|
content: 'This is your home planet. You can view its name, coordinates, and switch between planets here as you expand your empire.'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
title: 'Navigation Menu',
|
||||||
|
content:
|
||||||
|
'Use this menu to navigate between different sections: Buildings, Research, Fleet, Galaxy, and more. Each section offers unique gameplay features.'
|
||||||
|
},
|
||||||
|
gotoBuildings: {
|
||||||
|
title: 'Go to Buildings',
|
||||||
|
content: 'Let\'s start by building some structures. Click on the "Buildings" menu item to view available structures.'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: 'Build Solar Plant',
|
||||||
|
content:
|
||||||
|
'First, build a Solar Plant! It generates energy for your planet. Without energy, other resource buildings cannot function. This is the most important first step.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Build Queue',
|
||||||
|
content:
|
||||||
|
'Your building is now in the construction queue. Click the queue icon in the top-right corner to view all ongoing construction and research tasks. Buildings take time to complete, but you can continue working while waiting.'
|
||||||
|
},
|
||||||
|
buildMetalMine: {
|
||||||
|
title: 'Build Metal Mine',
|
||||||
|
content:
|
||||||
|
"Now that you have energy, you can build the Metal Mine. It's your primary source of metal, which is used in almost every structure and ship."
|
||||||
|
},
|
||||||
|
buildCrystalMine: {
|
||||||
|
title: 'Build Crystal Mine',
|
||||||
|
content: 'Crystal is rarer but essential for advanced technologies. Build a Crystal Mine to start collecting this valuable resource.'
|
||||||
|
},
|
||||||
|
buildDeuterium: {
|
||||||
|
title: 'Build Deuterium Synthesizer',
|
||||||
|
content:
|
||||||
|
'Deuterium is essential for ship fuel and advanced research. Build a Deuterium Synthesizer to start producing this critical resource.'
|
||||||
|
},
|
||||||
|
upgradeMines: {
|
||||||
|
title: 'Upgrade Resource Mines',
|
||||||
|
content:
|
||||||
|
'Next, you need to upgrade all three resource mines (Metal, Crystal, Deuterium) to level 2 to meet the requirements for building a Robotics Factory. Upgrade them when you have enough resources.'
|
||||||
|
},
|
||||||
|
buildRobotics: {
|
||||||
|
title: 'Build Robotics Factory',
|
||||||
|
content:
|
||||||
|
'The Robotics Factory significantly speeds up construction. It requires Metal Mine, Crystal Mine, and Deuterium Synthesizer at level 2 each. Build it to improve construction efficiency!'
|
||||||
|
},
|
||||||
|
upgradeMinesForLab: {
|
||||||
|
title: 'Continue Upgrading Mines',
|
||||||
|
content:
|
||||||
|
'Now you need to upgrade all three resource mines to level 3 to meet the requirements for the Research Lab. Keep developing your resource production.'
|
||||||
|
},
|
||||||
|
buildResearchLab: {
|
||||||
|
title: 'Build Research Lab',
|
||||||
|
content:
|
||||||
|
'The Research Lab is the foundation of technological advancement. It requires all three resource mines at level 3. Build it to unlock technology research!'
|
||||||
|
},
|
||||||
|
gotoResearch: {
|
||||||
|
title: 'Go to Research',
|
||||||
|
content: 'Now that you have a Research Lab, click on the "Research" menu to view available technologies.'
|
||||||
|
},
|
||||||
|
researchEnergy: {
|
||||||
|
title: 'Research Energy Technology',
|
||||||
|
content:
|
||||||
|
"Energy Technology improves your energy production and unlocks advanced structures. It's one of the most fundamental and important technologies."
|
||||||
|
},
|
||||||
|
shipyardIntro: {
|
||||||
|
title: 'Fleet and Shipyard',
|
||||||
|
content:
|
||||||
|
'Ships allow you to explore the galaxy, transport resources, and defend your empire. To build ships, you need a Shipyard (requires Robotics Factory level 2).'
|
||||||
|
},
|
||||||
|
gotoBuildingsForShipyard: {
|
||||||
|
title: 'Return to Buildings',
|
||||||
|
content: 'Go back to the Buildings page to construct your Shipyard.'
|
||||||
|
},
|
||||||
|
buildShipyard: {
|
||||||
|
title: 'Build Shipyard',
|
||||||
|
content: 'The Shipyard allows you to construct ships and defense systems. This is crucial for fleet operations.'
|
||||||
|
},
|
||||||
|
fleetIntro: {
|
||||||
|
title: 'Fleet Operations',
|
||||||
|
content:
|
||||||
|
'Once you have ships, you can send them on missions: transport resources, colonize planets, attack enemies, or explore debris fields.'
|
||||||
|
},
|
||||||
|
galaxyIntro: {
|
||||||
|
title: 'Explore the Galaxy',
|
||||||
|
content:
|
||||||
|
'The Galaxy view shows other planets, debris fields, and opportunities for expansion. Use it to scout targets and plan your strategy.'
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
title: 'Tutorial Complete!',
|
||||||
|
content:
|
||||||
|
'Congratulations, Commander! You now know the basics. Continue building your empire, research technologies, and explore the galaxy. Remember: develop energy first, then resources, then factories and research! Good luck!'
|
||||||
|
},
|
||||||
|
// Mobile tutorial
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: 'Welcome to OGame (Mobile)',
|
||||||
|
content:
|
||||||
|
"Welcome, Commander! This is a streamlined tutorial designed for touchscreens. We'll quickly cover the core features to get you started building your empire."
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
title: 'Top Resource Bar',
|
||||||
|
content: 'At the top, you see your resources: Metal, Crystal, and Deuterium. Tap to view detailed production info.'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
title: 'Open Navigation Menu',
|
||||||
|
content:
|
||||||
|
'Tap this menu icon to open the navigation bar. You can access Buildings, Research, Fleet, and all other features from here.'
|
||||||
|
},
|
||||||
|
gotoBuildings: {
|
||||||
|
title: 'Go to Buildings Page',
|
||||||
|
content: 'The menu is now open! Now tap "Buildings" to start constructing your infrastructure.'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: 'Build Solar Plant',
|
||||||
|
content: 'First, build a Solar Plant! Scroll down to find it and tap the card to build. Energy is the foundation of everything.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Build Queue',
|
||||||
|
content:
|
||||||
|
'Click the queue icon in the top-right corner to view build progress. You can continue browsing other pages - construction happens in the background.'
|
||||||
|
},
|
||||||
|
buildMetalMine: {
|
||||||
|
title: 'Build Metal Mine',
|
||||||
|
content: 'Now that you have energy, build a Metal Mine. Scroll down to find it and tap to build.'
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
title: 'Quick Tutorial Complete!',
|
||||||
|
content:
|
||||||
|
"Great! You've mastered the basics. Continue building Crystal and Deuterium facilities, then explore other features. Remember: energy first, then resources!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: 'Close',
|
||||||
|
gotIt: 'Got it',
|
||||||
|
dontShowAgain: "Don't show again",
|
||||||
|
resetHints: 'Reset Hints',
|
||||||
|
resetHintsDesc: 'Show all hints again',
|
||||||
|
hintsEnabled: 'Enable Hints',
|
||||||
|
hintsEnabledDesc: 'Show helpful hints when visiting pages',
|
||||||
|
overview: {
|
||||||
|
title: 'Planet Overview',
|
||||||
|
message: 'Here you can see your planet resources, fleet status, and production details. Check back often to monitor your progress!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: 'Buildings',
|
||||||
|
message:
|
||||||
|
'Build and upgrade structures here. Start with Solar Plant for energy, then resource mines. Tip: Robotics Factory speeds up construction!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: 'Research Lab',
|
||||||
|
message:
|
||||||
|
'Research technologies to unlock new ships, improve combat, and advance your civilization. Energy Technology is a great start!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: 'Shipyard',
|
||||||
|
message: 'Build ships to explore, transport resources, and defend your empire. Cargo ships help move resources between planets.'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: 'Fleet Command',
|
||||||
|
message: 'Send your ships on missions: attack enemies, transport resources, colonize new planets, or explore debris fields.'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: 'Galaxy Map',
|
||||||
|
message:
|
||||||
|
'Explore the galaxy to find empty planets to colonize, debris fields to harvest, and enemies to attack. Use spy probes first!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: 'Diplomacy',
|
||||||
|
message:
|
||||||
|
'Manage relations with NPCs. Send gifts to improve reputation, or face hostile attacks. Allies of your enemies may turn hostile too!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: 'Messages',
|
||||||
|
message: 'View battle reports, spy reports, and diplomatic notifications here. Keep track of your activities and enemy encounters.'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: 'Planetary Defense',
|
||||||
|
message: 'Build defense structures to protect your planet from attacks. Shield domes and turrets can deter raiders!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: 'Officers',
|
||||||
|
message:
|
||||||
|
'Recruit officers to gain various bonuses! Commander speeds up construction, Geologist boosts resource production, Admiral enhances fleet capabilities.'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: 'Battle Simulator',
|
||||||
|
message: 'Simulate battle outcomes before attacking. Enter both fleets and tech levels to predict victory, losses, and loot.'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Settings',
|
||||||
|
message: 'Manage game data, adjust notifications, export/import saves here. Remember to backup your progress regularly!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM Panel',
|
||||||
|
message:
|
||||||
|
'GM mode allows quick modification of resources, buildings, and tech levels. Use it for testing or experiencing full game content.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: '星々を征服せよ',
|
||||||
|
startGame: 'ゲーム開始',
|
||||||
|
privacyAgreement: 'プライバシーポリシー',
|
||||||
|
privacyAgreementDesc: 'ゲームを開始する前に、プライバシーポリシーをお読みになり、同意してください。',
|
||||||
|
agreeToPrivacy: '読んで同意しました',
|
||||||
|
viewFullPolicy: '全文を見る',
|
||||||
|
agreeAndStart: '同意して開始'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: '確認',
|
confirm: '確認',
|
||||||
cancel: 'キャンセル',
|
cancel: 'キャンセル',
|
||||||
@@ -8,6 +17,7 @@ export default {
|
|||||||
close: '閉じる',
|
close: '閉じる',
|
||||||
back: '戻る',
|
back: '戻る',
|
||||||
next: '次へ',
|
next: '次へ',
|
||||||
|
gotIt: '',
|
||||||
previous: '前へ',
|
previous: '前へ',
|
||||||
submit: '送信',
|
submit: '送信',
|
||||||
reset: 'リセット',
|
reset: 'リセット',
|
||||||
@@ -31,10 +41,12 @@ export default {
|
|||||||
goToBuildings: '建物へ移動',
|
goToBuildings: '建物へ移動',
|
||||||
locked: 'ロック済み',
|
locked: 'ロック済み',
|
||||||
viewRequirements: '必要条件を表示',
|
viewRequirements: '必要条件を表示',
|
||||||
|
requirements: '必要条件',
|
||||||
requirementsNotMet: '必要条件が満たされていません',
|
requirementsNotMet: '必要条件が満たされていません',
|
||||||
current: '現在',
|
current: '現在',
|
||||||
level: 'レベル',
|
level: 'レベル',
|
||||||
gmModeActivated: 'GMモードが有効になりました!ナビゲーションメニューをご確認ください。'
|
gmModeActivated: 'GMモードが有効になりました!ナビゲーションメニューをご確認ください。',
|
||||||
|
view: '表示'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: '前提条件を満たしていません',
|
requirementsNotMet: '前提条件を満たしていません',
|
||||||
@@ -56,7 +68,8 @@ export default {
|
|||||||
buildingLevelZero: '建物レベルが0のため、解体できません',
|
buildingLevelZero: '建物レベルが0のため、解体できません',
|
||||||
researchQueueFull: '研究キューが満杯です',
|
researchQueueFull: '研究キューが満杯です',
|
||||||
moonExists: '月は既に存在します',
|
moonExists: '月は既に存在します',
|
||||||
insufficientDebris: '残骸フィールドが不足しています'
|
insufficientDebris: '残骸フィールドが不足しています',
|
||||||
|
launchFailed: '発射に失敗しました'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: '概要',
|
overview: '概要',
|
||||||
@@ -111,7 +124,13 @@ export default {
|
|||||||
homePlanet: '母星',
|
homePlanet: '母星',
|
||||||
planetPrefix: '惑星',
|
planetPrefix: '惑星',
|
||||||
moonSuffix: 'の月',
|
moonSuffix: 'の月',
|
||||||
colonyPrefix: 'コロニー'
|
colonyPrefix: 'コロニー',
|
||||||
|
renamePlanet: '惑星名を変更',
|
||||||
|
renamePlanetTitle: '惑星名を変更',
|
||||||
|
newPlanetName: '新しい名前',
|
||||||
|
planetNamePlaceholder: '新しい惑星名を入力',
|
||||||
|
rename: '名前変更',
|
||||||
|
renameSuccess: '惑星名を {name} に変更しました'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: '総ポイント'
|
points: '総ポイント'
|
||||||
@@ -125,6 +144,7 @@ export default {
|
|||||||
roboticsFactory: 'ロボット工場',
|
roboticsFactory: 'ロボット工場',
|
||||||
naniteFactory: 'ナノマシン工場',
|
naniteFactory: 'ナノマシン工場',
|
||||||
shipyard: '造船所',
|
shipyard: '造船所',
|
||||||
|
hangar: '格納庫',
|
||||||
researchLab: '研究所',
|
researchLab: '研究所',
|
||||||
metalStorage: '金属倉庫',
|
metalStorage: '金属倉庫',
|
||||||
crystalStorage: 'クリスタル倉庫',
|
crystalStorage: 'クリスタル倉庫',
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
jumpGate: 'ジャンプゲート',
|
jumpGate: 'ジャンプゲート',
|
||||||
planetDestroyerFactory: '惑星破壊工場',
|
planetDestroyerFactory: '惑星破壊工場',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
production: '生産量',
|
production: '生産量',
|
||||||
consumption: '消費',
|
consumption: '消費',
|
||||||
totalCost: '総コスト',
|
totalCost: '総コスト',
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
roboticsFactory: '建設速度を向上',
|
roboticsFactory: '建設速度を向上',
|
||||||
naniteFactory: '建設キュー数を増加、レベル毎に+1(最大10レベル)',
|
naniteFactory: '建設キュー数を増加、レベル毎に+1(最大10レベル)',
|
||||||
shipyard: '艦船を建造',
|
shipyard: '艦船を建造',
|
||||||
|
hangar: '艦隊収容能力を拡張する専用施設、惑星の専門化をサポート',
|
||||||
researchLab: '技術を研究',
|
researchLab: '技術を研究',
|
||||||
metalStorage: '金属の貯蔵上限を増加',
|
metalStorage: '金属の貯蔵上限を増加',
|
||||||
crystalStorage: 'クリスタルの貯蔵上限を増加',
|
crystalStorage: 'クリスタルの貯蔵上限を増加',
|
||||||
@@ -199,10 +221,10 @@ export default {
|
|||||||
lightFighter: '基本戦闘ユニット',
|
lightFighter: '基本戦闘ユニット',
|
||||||
heavyFighter: '重装甲戦闘機',
|
heavyFighter: '重装甲戦闘機',
|
||||||
cruiser: '中型戦艦、攻守バランス型',
|
cruiser: '中型戦艦、攻守バランス型',
|
||||||
battleship: '強力な戦艦',
|
battleship: '主力重戦艦、強力な火力と高い防御力を持つ',
|
||||||
battlecruiser: '高速強力な戦闘艦、戦艦への攻撃に優れる',
|
battlecruiser: '高速強力な戦闘艦、戦艦への攻撃に優れる',
|
||||||
bomber: '防御施設への攻撃に特化した艦船',
|
bomber: '防御施設への攻撃に特化した艦船',
|
||||||
destroyer: '大型艦の破壊に特化したハンター',
|
destroyer: '対大型艦専用艦、高火力だが防御力が低い',
|
||||||
smallCargo: '少量の資源を輸送',
|
smallCargo: '少量の資源を輸送',
|
||||||
largeCargo: '大量の資源を輸送',
|
largeCargo: '大量の資源を輸送',
|
||||||
colonyShip: '新惑星の植民に使用',
|
colonyShip: '新惑星の植民に使用',
|
||||||
@@ -278,8 +300,9 @@ export default {
|
|||||||
ionTechnology: 'イオン兵器技術',
|
ionTechnology: 'イオン兵器技術',
|
||||||
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
hyperspaceTechnology: 'ハイパースペースジャンプ技術',
|
||||||
plasmaTechnology: 'プラズマ兵器技術',
|
plasmaTechnology: 'プラズマ兵器技術',
|
||||||
computerTechnology: '研究キュー数を増加、レベル毎に+1(最大10レベル)',
|
computerTechnology: '研究キューと艦隊任務スロットを増加、レベル毎に+1キュー+1スロット(最大10レベル)',
|
||||||
espionageTechnology: 'スパイ探査機の効果を向上、レベル毎に偵察深度+1',
|
espionageTechnology:
|
||||||
|
'スパイ探査機の効果を向上、レベル毎に偵察深度+1。偵察レベル=自分のレベル-相手のレベル+探査機数/5。≥-1で艦隊表示、≥1で防御表示、≥3で建物表示、≥5で技術表示',
|
||||||
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
weaponsTechnology: '艦船と防御の攻撃力をレベル毎に10%増加',
|
||||||
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
shieldingTechnology: '艦船と防御のシールドをレベル毎に10%増加',
|
||||||
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
armourTechnology: '艦船と防御の装甲をレベル毎に10%増加',
|
||||||
@@ -312,10 +335,13 @@ export default {
|
|||||||
darkMatterSpecialist: 'ダークマター採取効率を向上'
|
darkMatterSpecialist: 'ダークマター採取効率を向上'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: '進行中のタスク',
|
||||||
|
empty: '進行中のキューはありません',
|
||||||
buildQueue: '建設キュー',
|
buildQueue: '建設キュー',
|
||||||
researchQueue: '研究キュー',
|
researchQueue: '研究キュー',
|
||||||
building: '建設中',
|
building: '建設中',
|
||||||
researching: '研究中',
|
researching: '研究中',
|
||||||
|
demolishing: '解体中',
|
||||||
remaining: '残り時間',
|
remaining: '残り時間',
|
||||||
cancel: 'キャンセル',
|
cancel: 'キャンセル',
|
||||||
cancelBuild: '建設キャンセル',
|
cancelBuild: '建設キャンセル',
|
||||||
@@ -323,7 +349,14 @@ export default {
|
|||||||
confirmCancel: 'キャンセルしますか?資源の50%が返還されます。',
|
confirmCancel: 'キャンセルしますか?資源の50%が返還されます。',
|
||||||
level: 'レベル',
|
level: 'レベル',
|
||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeToLevel: 'レベルにアップグレード'
|
upgradeToLevel: 'レベルにアップグレード',
|
||||||
|
tabs: {
|
||||||
|
all: 'すべて',
|
||||||
|
buildings: '建物',
|
||||||
|
research: '研究',
|
||||||
|
ships: '艦船',
|
||||||
|
defense: '防衛'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shipyard: {
|
shipyard: {
|
||||||
attack: '攻撃力',
|
attack: '攻撃力',
|
||||||
@@ -335,6 +368,7 @@ export default {
|
|||||||
fuelConsumption: '燃料消費',
|
fuelConsumption: '燃料消費',
|
||||||
buildCost: '建設コスト',
|
buildCost: '建設コスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
perUnit: 'ユニットあたり',
|
perUnit: 'ユニットあたり',
|
||||||
batchCalculator: '一括計算機',
|
batchCalculator: '一括計算機',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
@@ -362,6 +396,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'アップグレードコスト',
|
upgradeCost: 'アップグレードコスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '建設',
|
||||||
upgrade: 'アップグレード',
|
upgrade: 'アップグレード',
|
||||||
maxLevelReached: '最大レベルに達しました',
|
maxLevelReached: '最大レベルに達しました',
|
||||||
requirementsNotMet: '要件が満たされていません',
|
requirementsNotMet: '要件が満たされていません',
|
||||||
@@ -389,6 +424,7 @@ export default {
|
|||||||
armor: '装甲',
|
armor: '装甲',
|
||||||
buildCost: '建設コスト',
|
buildCost: '建設コスト',
|
||||||
buildTime: '建設時間',
|
buildTime: '建設時間',
|
||||||
|
build: '',
|
||||||
perUnit: 'ユニットあたり',
|
perUnit: 'ユニットあたり',
|
||||||
batchCalculator: '一括計算機',
|
batchCalculator: '一括計算機',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
@@ -461,6 +497,7 @@ export default {
|
|||||||
colonize: '植民',
|
colonize: '植民',
|
||||||
spy: '偵察',
|
spy: '偵察',
|
||||||
deploy: '配備',
|
deploy: '配備',
|
||||||
|
expedition: '探検',
|
||||||
recycle: '回収',
|
recycle: '回収',
|
||||||
transportResources: '資源輸送',
|
transportResources: '資源輸送',
|
||||||
totalCargoCapacity: '総積載量',
|
totalCargoCapacity: '総積載量',
|
||||||
@@ -573,17 +610,20 @@ export default {
|
|||||||
sendGift: 'ギフト送信',
|
sendGift: 'ギフト送信',
|
||||||
debris: '破片',
|
debris: '破片',
|
||||||
giftPlanetTitle: 'ギフト送信',
|
giftPlanetTitle: 'ギフト送信',
|
||||||
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。'
|
giftPlanetMessage: '惑星[{coordinates}]にリソースを贈りますか?\n\n艦隊ページに移動して輸送船を選択し、リソースを積載してください。',
|
||||||
|
npcPlanetName: '{name}の惑星'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: 'メッセージセンター',
|
title: 'メッセージセンター',
|
||||||
battles: '戦闘',
|
battles: '戦闘',
|
||||||
spy: 'スパイ',
|
spy: 'スパイ',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
battleReports: '戦闘レポート',
|
battleReports: '戦闘レポート',
|
||||||
spyReports: 'スパイレポート',
|
spyReports: 'スパイレポート',
|
||||||
noBattleReports: '戦闘レポートなし',
|
noBattleReports: '戦闘レポートなし',
|
||||||
noSpyReports: 'スパイレポートなし',
|
noSpyReports: 'スパイレポートなし',
|
||||||
|
noDiplomaticReports: '',
|
||||||
battleReport: '戦闘レポート',
|
battleReport: '戦闘レポート',
|
||||||
spyReport: 'スパイレポート',
|
spyReport: 'スパイレポート',
|
||||||
victory: '勝利',
|
victory: '勝利',
|
||||||
@@ -605,6 +645,7 @@ export default {
|
|||||||
targetPlanet: '目標惑星',
|
targetPlanet: '目標惑星',
|
||||||
attackerRemaining: '攻撃側残存',
|
attackerRemaining: '攻撃側残存',
|
||||||
defenderRemaining: '防御側残存',
|
defenderRemaining: '防御側残存',
|
||||||
|
allDestroyed: '全て破壊',
|
||||||
moonChance: '月生成確率',
|
moonChance: '月生成確率',
|
||||||
showRoundDetails: 'ラウンド詳細表示',
|
showRoundDetails: 'ラウンド詳細表示',
|
||||||
hideRoundDetails: 'ラウンド詳細非表示',
|
hideRoundDetails: 'ラウンド詳細非表示',
|
||||||
@@ -639,7 +680,48 @@ export default {
|
|||||||
hostile: '相手は敵対的でギフトを受け取りません',
|
hostile: '相手は敵対的でギフトを受け取りません',
|
||||||
neutral_distrust: '相手はあなたを信頼していません',
|
neutral_distrust: '相手はあなたを信頼していません',
|
||||||
polite_decline: '丁重に断りました'
|
polite_decline: '丁重に断りました'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: '',
|
||||||
|
spyDetected: '',
|
||||||
|
detectionResult: '',
|
||||||
|
detectionSuccess: '',
|
||||||
|
spiedNotificationMessage: '',
|
||||||
|
spiedNotificationTip: '',
|
||||||
|
viewInGalaxy: '',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: '',
|
||||||
|
missionSuccess: '',
|
||||||
|
missionFailed: '',
|
||||||
|
origin: '',
|
||||||
|
destination: '',
|
||||||
|
missionDetails: '',
|
||||||
|
transportedResources: '',
|
||||||
|
recycledResources: '',
|
||||||
|
remainingDebris: '',
|
||||||
|
newPlanet: '',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: '',
|
||||||
|
activityType: {
|
||||||
|
recycle: ''
|
||||||
|
},
|
||||||
|
activityLocation: '',
|
||||||
|
position: '',
|
||||||
|
nearPlanet: '',
|
||||||
|
activityDescription: '',
|
||||||
|
npcActivityMessage: '',
|
||||||
|
arrivalTime: '',
|
||||||
|
npcActivityTip: '',
|
||||||
|
clearMessages: 'メッセージをクリア',
|
||||||
|
clearMessageTypes: 'クリアするメッセージタイプを選択',
|
||||||
|
clearBattleReports: '戦闘レポート',
|
||||||
|
clearSpyReports: '偵察レポート',
|
||||||
|
clearSpiedNotifications: '偵察通知',
|
||||||
|
clearMissionReports: 'ミッションレポート',
|
||||||
|
clearNPCActivity: 'NPCアクティビティ',
|
||||||
|
clearGiftNotifications: 'ギフト通知',
|
||||||
|
clearGiftRejected: '拒否されたギフト',
|
||||||
|
clearNow: '今すぐクリア'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: '輸送ミッションが正常に完了しました',
|
transportSuccess: '輸送ミッションが正常に完了しました',
|
||||||
@@ -651,7 +733,19 @@ export default {
|
|||||||
recycleSuccess: '回収ミッションが正常に完了しました',
|
recycleSuccess: '回収ミッションが正常に完了しました',
|
||||||
recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません',
|
recycleFailed: '回収ミッションが失敗しました。目標位置にデブリがありません',
|
||||||
destroySuccess: '惑星破壊ミッションが正常に実行されました',
|
destroySuccess: '惑星破壊ミッションが正常に実行されました',
|
||||||
destroyFailed: '惑星破壊ミッションが失敗しました'
|
destroyFailed: '惑星破壊ミッションが失敗しました',
|
||||||
|
missileAttackSuccess: 'ミサイル攻撃成功',
|
||||||
|
missileAttackFailed: 'ミサイル攻撃失敗、目標惑星が存在しません',
|
||||||
|
missileAttackIntercepted: '全てのミサイルが迎撃されました',
|
||||||
|
hits: '命中',
|
||||||
|
expeditionResources: '探検隊が資源を発見しました!',
|
||||||
|
expeditionDarkMatter: '探検隊がダークマターを発見しました!',
|
||||||
|
expeditionFleet: '探検隊が廃棄された艦船を発見しました!',
|
||||||
|
expeditionPiratesAttack: '探検隊が海賊に襲撃され、一部の艦船を失いました',
|
||||||
|
expeditionPiratesEscaped: '探検隊が海賊に遭遇しましたが、無事に脱出しました',
|
||||||
|
expeditionAliensAttack: '探検隊がエイリアンに襲撃され、一部の艦船を失いました',
|
||||||
|
expeditionAliensEscaped: '探検隊がエイリアンに遭遇しましたが、無事に脱出しました',
|
||||||
|
expeditionNothing: '探検隊は何も発見できませんでした'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: '戦闘シミュレーター',
|
title: '戦闘シミュレーター',
|
||||||
@@ -741,7 +835,27 @@ export default {
|
|||||||
buildDate: 'ビルド日',
|
buildDate: 'ビルド日',
|
||||||
community: 'コミュニティ',
|
community: 'コミュニティ',
|
||||||
github: 'GitHubリポジトリ',
|
github: 'GitHubリポジトリ',
|
||||||
qqGroup: 'QQグループ'
|
qqGroup: 'QQグループ',
|
||||||
|
privacyPolicy: 'プライバシーポリシー',
|
||||||
|
notifications: '通知設定',
|
||||||
|
notificationsDesc: 'ゲーム内の通知アラートを管理',
|
||||||
|
notificationTypes: '通知タイプ',
|
||||||
|
browserNotifications: 'ブラウザ通知',
|
||||||
|
inAppNotifications: 'アプリ内通知',
|
||||||
|
constructionComplete: '建設完了',
|
||||||
|
researchComplete: '研究完了',
|
||||||
|
browserPermission: 'ブラウザ通知を有効にする',
|
||||||
|
permissionGranted: '許可されました',
|
||||||
|
permissionDenied: '許可が拒否されたか、付与されていません',
|
||||||
|
inAppNotificationsDesc: 'ページ内ポップアップで表示',
|
||||||
|
notificationsDisabled: '特定の通知を設定するには、上記のスイッチを有効にしてください',
|
||||||
|
suppressInFocus: 'ページにフォーカスがある場合、ブラウザ通知を抑制する',
|
||||||
|
expandTypes: '詳細を展開',
|
||||||
|
collapseTypes: '詳細を折りたたむ'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: '建設完了',
|
||||||
|
researchComplete: '研究完了'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GMコントロールパネル',
|
title: 'GMコントロールパネル',
|
||||||
@@ -804,17 +918,42 @@ export default {
|
|||||||
completeQueuesSuccess: ''
|
completeQueuesSuccess: ''
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count}機の敵艦隊が接近中',
|
||||||
npcSpyIncoming: 'NPC偵察プローブが接近中',
|
npcSpyIncoming: 'NPC偵察プローブが接近中',
|
||||||
npcAttackIncoming: 'NPC艦隊攻撃が接近中!',
|
npcAttackIncoming: 'NPC艦隊攻撃が接近中!',
|
||||||
npcFleetIncoming: 'NPC艦隊が接近中',
|
npcFleetIncoming: 'NPC艦隊が接近中',
|
||||||
ships: '隻',
|
ships: '隻',
|
||||||
spiedBy: '偵察された',
|
spiedBy: '偵察された',
|
||||||
attackedBy: '攻撃された',
|
attackedBy: '攻撃された',
|
||||||
detectionSuccess: '偵察が発見された',
|
detectionSuccess: '敵の偵察を発見した!',
|
||||||
detectionFailed: '偵察が発見されなかった',
|
detectionFailed: '偵察が発見されなかった',
|
||||||
npcSpiedYourPlanet: 'NPCがあなたの惑星を偵察しました',
|
npcSpiedYourPlanet: 'NPCがあなたの惑星を偵察しました',
|
||||||
npcAttackedYourPlanet: 'NPCがあなたの惑星を攻撃しました'
|
npcAttackedYourPlanet: 'NPCがあなたの惑星を攻撃しました'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: '敵警報',
|
||||||
|
markAllRead: 'すべて既読',
|
||||||
|
noAlerts: '警報なし',
|
||||||
|
fleetSize: '艦隊規模',
|
||||||
|
ships: '隻',
|
||||||
|
viewFleet: '艦隊を見る',
|
||||||
|
alertDetails: '警報詳細',
|
||||||
|
targetInfo: 'ターゲット情報',
|
||||||
|
arrivalTime: '到着時間',
|
||||||
|
countdown: 'カウントダウン',
|
||||||
|
viewMessages: 'メッセージを見る',
|
||||||
|
arrived: '到着済み',
|
||||||
|
missionType: {
|
||||||
|
spy: '偵察',
|
||||||
|
attack: '攻撃',
|
||||||
|
unknown: '不明'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: '敵の偵察が接近中!',
|
||||||
|
attack: '敵の攻撃が接近中!',
|
||||||
|
unknown: '敵艦隊が接近中!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: '外交',
|
title: '外交',
|
||||||
description: 'NPCとの外交関係を管理',
|
description: 'NPCとの外交関係を管理',
|
||||||
@@ -831,6 +970,10 @@ export default {
|
|||||||
recentEvents: '最近のイベント',
|
recentEvents: '最近のイベント',
|
||||||
recentEventsDescription: '最近の外交活動ログ',
|
recentEventsDescription: '最近の外交活動ログ',
|
||||||
ago: '前',
|
ago: '前',
|
||||||
|
notifications: '',
|
||||||
|
markAllRead: '',
|
||||||
|
noReports: '',
|
||||||
|
viewAll: '',
|
||||||
status: {
|
status: {
|
||||||
friendly: '友好的',
|
friendly: '友好的',
|
||||||
neutral: '中立',
|
neutral: '中立',
|
||||||
@@ -843,9 +986,30 @@ export default {
|
|||||||
more: 'その他',
|
more: 'その他',
|
||||||
actions: {
|
actions: {
|
||||||
gift: 'ギフトを送る',
|
gift: 'ギフトを送る',
|
||||||
viewPlanets: '惑星を表示'
|
viewPlanets: '惑星を表示',
|
||||||
|
addNote: 'メモを追加',
|
||||||
|
editNote: 'メモを編集'
|
||||||
},
|
},
|
||||||
|
note: 'メモ',
|
||||||
|
notePlaceholder: 'メモを入力...',
|
||||||
|
noteEmpty: 'メモなし',
|
||||||
lastEvent: '最後のイベント',
|
lastEvent: '最後のイベント',
|
||||||
|
reportDetails: '外交レポート詳細',
|
||||||
|
eventDescription: 'イベント説明',
|
||||||
|
reputationChange: '評判変化',
|
||||||
|
before: '前',
|
||||||
|
after: '後',
|
||||||
|
statusChange: '関係状態変化',
|
||||||
|
viewDiplomacy: '外交ページを表示',
|
||||||
|
eventType: {
|
||||||
|
gift: 'リソースを贈呈',
|
||||||
|
attack: '攻撃を開始',
|
||||||
|
allyAttacked: '同盟を攻撃',
|
||||||
|
spy: '偵察を実施',
|
||||||
|
stealDebris: '残骸を略奪',
|
||||||
|
destroyPlanet: '惑星を破壊',
|
||||||
|
unknown: '不明なイベント'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: 'ギフト送信',
|
gift: 'ギフト送信',
|
||||||
attack: '攻撃',
|
attack: '攻撃',
|
||||||
@@ -874,12 +1038,63 @@ export default {
|
|||||||
receivedGiftFromNpc: '{npcName}からギフトを受け取りました',
|
receivedGiftFromNpc: '{npcName}からギフトを受け取りました',
|
||||||
acceptedGiftFromNpc: '{npcName}からのギフトを受け取りました:{metal}M {crystal}C {deuterium}D',
|
acceptedGiftFromNpc: '{npcName}からのギフトを受け取りました:{metal}M {crystal}C {deuterium}D',
|
||||||
playerRejectedGift: 'プレイヤーがギフトを拒否しました',
|
playerRejectedGift: 'プレイヤーがギフトを拒否しました',
|
||||||
rejectedGiftFromNpc: '{npcName}からのギフトを拒否しました。評判{reputation}'
|
rejectedGiftFromNpc: '{npcName}からのギフトを拒否しました。評判{reputation}',
|
||||||
|
destroyedNpcPlanet: '{npcName}の{planetName}を破壊しました',
|
||||||
|
playerDestroyedPlanet: 'プレイヤーが{planetName}を破壊しました',
|
||||||
|
youDestroyedNpcPlanet: 'あなたは{npcName}の{planetName}を破壊しました。評判{reputation}',
|
||||||
|
playerDestroyedAllyPlanet: 'プレイヤーが同盟{allyName}の{planetName}を破壊しました',
|
||||||
|
allyOutraged: '{allyName}はあなたが同盟{targetName}の{planetName}を破壊したことに激怒しています',
|
||||||
|
npcEliminated: 'NPC {npcName}は完全に排除されました',
|
||||||
|
npcEliminatedMessage: 'あなたは{npcName}のすべての惑星を破壊しました!この勢力は完全に壊滅しました。'
|
||||||
|
},
|
||||||
|
searchPlaceholder: 'NPC名で検索...',
|
||||||
|
viewMode: {
|
||||||
|
card: 'カード',
|
||||||
|
list: 'リスト'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC状態診断',
|
||||||
|
title: 'NPC状態診断',
|
||||||
|
description: 'プレイヤーポイント:{points}、偵察間隔:{spyInterval}分、攻撃間隔:{attackInterval}分、攻撃確率:{attackProb}%',
|
||||||
|
noData: 'NPCデータがありません',
|
||||||
|
difficulty: '難易度',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: '簡単',
|
||||||
|
medium: '普通',
|
||||||
|
hard: '難しい'
|
||||||
|
},
|
||||||
|
reputation: '評判',
|
||||||
|
spyProbes: '偵察機数',
|
||||||
|
fleetPower: '艦隊戦力',
|
||||||
|
canSpy: '偵察可能',
|
||||||
|
canAttack: '攻撃可能',
|
||||||
|
attackProbability: '攻撃確率',
|
||||||
|
nextSpy: '次の偵察',
|
||||||
|
nextAttack: '次の攻撃',
|
||||||
|
yes: 'はい',
|
||||||
|
no: 'いいえ',
|
||||||
|
timeFormat: '{min}分{sec}秒',
|
||||||
|
anytime: 'いつでも',
|
||||||
|
statusExplanation: '状態説明',
|
||||||
|
noRelation: '関係なし',
|
||||||
|
noRelationNeutral: '関係なし(中立)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: '友好関係、行動しない',
|
||||||
|
neutralNoAction: '中立関係、行動しない',
|
||||||
|
hostileWillAct: '敵対関係、行動する可能性あり',
|
||||||
|
noRelationNeutral: '外交関係なし、中立として扱う',
|
||||||
|
insufficientProbes: '偵察機不足(現在:{current}、必要:{required})',
|
||||||
|
noFleet: '戦闘艦隊がない',
|
||||||
|
spyCooldown: '偵察クールダウン中({min}分{sec}秒)',
|
||||||
|
attackCooldown: '攻撃クールダウン中({min}分{sec}秒)',
|
||||||
|
notSpiedYet: '未偵察、先に偵察が必要'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: '前へ',
|
previous: '前へ',
|
||||||
next: '次へ',
|
next: '次へ',
|
||||||
|
gotIt: '',
|
||||||
first: '最初',
|
first: '最初',
|
||||||
last: '最後',
|
last: '最後',
|
||||||
page: '{page}ページ'
|
page: '{page}ページ'
|
||||||
@@ -888,5 +1103,148 @@ export default {
|
|||||||
title: 'ページが見つかりません',
|
title: 'ページが見つかりません',
|
||||||
description: '申し訳ございません。お探しのページは存在しません',
|
description: '申し訳ございません。お探しのページは存在しません',
|
||||||
goHome: 'ホームに戻る'
|
goHome: 'ホームに戻る'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: 'プライバシーポリシー',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: 'はじめに',
|
||||||
|
content:
|
||||||
|
'このプライバシーポリシーは、OGame-Vue-Tsがお客様のデータをどのように取り扱うかを説明しています。私たちはお客様のプライバシーの保護に取り組んでおり、このゲームはユーザーのプライバシーを完全に尊重して設計されています。'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: 'データ収集',
|
||||||
|
content: 'このゲームは、以下のデータのみをブラウザにローカルで収集・保存します:',
|
||||||
|
items: {
|
||||||
|
gameProgress: 'ゲームの進行状況(建物レベル、艦隊、資源など)',
|
||||||
|
settings: 'ゲーム設定(通知設定、表示オプションなど)',
|
||||||
|
language: '言語設定'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: 'データ保存',
|
||||||
|
content:
|
||||||
|
'すべてのデータはブラウザのローカルストレージ(localStorage)に保存されます。これは、お客様のデータが常にお客様自身のデバイスに留まることを意味し、私たちはお客様のゲームデータにアクセス、閲覧、収集することはできません。'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: 'サーバー通信なし',
|
||||||
|
content:
|
||||||
|
'このゲームは完全にオフラインのシングルプレイヤーゲームです。アップデート確認機能(GitHubからバージョン情報を取得)を除き、ゲームはいかなるサーバーとも通信しません。お客様のゲームデータがデバイスから離れることはありません。'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: 'サードパーティサービス',
|
||||||
|
content:
|
||||||
|
'このゲームは、訪問者統計とトラフィックソースを追跡するためにサードパーティの分析サービスを使用しています。これにより、利用パターンを理解し、ゲーム体験を向上させることができます。この分析データは匿名であり、個人を特定できる情報は含まれていません。広告サービスやその他の商業的追跡ツールは使用していません。'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: 'データ管理',
|
||||||
|
content: 'お客様はご自身のデータを完全に管理できます:',
|
||||||
|
items: {
|
||||||
|
export: 'いつでもゲームデータをエクスポートできます',
|
||||||
|
import: 'バックアップファイルからデータをインポートできます',
|
||||||
|
delete: 'ブラウザデータの削除またはゲーム内の「データ削除」機能を使用してすべてのデータを削除できます'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: 'お問い合わせ',
|
||||||
|
content: 'このプライバシーポリシーについてご質問がある場合は、以下よりお問い合わせください:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: '日',
|
||||||
|
hours: '時間',
|
||||||
|
minutes: '分',
|
||||||
|
seconds: '秒'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
welcome: {
|
||||||
|
title: 'OGame-Vue-Ts へようこそ',
|
||||||
|
content: 'ようこそ、司令官!基礎から始めて、宇宙帝国を築きましょう。'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: '太陽光発電所を建設',
|
||||||
|
content:
|
||||||
|
'まず太陽光発電所を建設しましょう!惑星にエネルギーを供給します。エネルギーがないと、他の資源施設が機能しません。これが最も重要な第一歩です。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建設キュー',
|
||||||
|
content:
|
||||||
|
'建物は建設キューに追加されました。右上のキューアイコンをクリックすると、進行中のすべての建設と研究タスクを確認できます。建設には時間がかかりますが、待機中も作業を続けられます。'
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: 'OGame-Vue-Ts へようこそ(モバイル版)',
|
||||||
|
content:
|
||||||
|
'ようこそ、司令官!これはタッチスクリーン向けに最適化された簡易チュートリアルです。帝国建設を始めるために、コア機能を素早くご紹介します。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建設キュー',
|
||||||
|
content:
|
||||||
|
'右上のキューアイコンをクリックして建設進度を確認できます。他のページを閲覧し続けることができます。建設はバックグラウンドで進行します。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: '閉じる',
|
||||||
|
gotIt: '了解',
|
||||||
|
dontShowAgain: '今後表示しない',
|
||||||
|
resetHints: 'ヒントをリセット',
|
||||||
|
resetHintsDesc: 'すべてのヒントを再表示',
|
||||||
|
hintsEnabled: 'ヒントを有効化',
|
||||||
|
hintsEnabledDesc: 'ページ訪問時にヘルプヒントを表示',
|
||||||
|
overview: {
|
||||||
|
title: '惑星概要',
|
||||||
|
message: 'ここで惑星の資源、艦隊状況、生産詳細を確認できます。進捗を監視するために定期的にチェックしましょう!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: '建物',
|
||||||
|
message:
|
||||||
|
'ここで建物を建設・アップグレードします。まず太陽光発電所でエネルギーを確保し、次に資源鉱山を建設。ヒント:ロボット工場で建設速度アップ!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: '研究ラボ',
|
||||||
|
message: '技術を研究して新しい船を解放、戦闘力を向上、文明を発展させましょう。エネルギー技術から始めるのがおすすめ!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: '造船所',
|
||||||
|
message: '船を建造して探索、資源輸送、帝国防衛に活用。貨物船は惑星間で資源を運びます。'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: '艦隊司令',
|
||||||
|
message: '船をミッションに派遣:敵を攻撃、資源を輸送、新惑星を植民、または残骸場を探索。'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: '銀河マップ',
|
||||||
|
message: '銀河を探索して植民可能な空き惑星、回収可能な残骸場、攻撃対象の敵を見つけましょう。まずスパイプローブで偵察!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: '外交',
|
||||||
|
message: 'NPCとの関係を管理。贈り物で評判を上げるか、敵対攻撃を受けるか。敵の同盟者もあなたに敵対するかも!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: 'メッセージ',
|
||||||
|
message: 'ここで戦闘レポート、スパイレポート、外交通知を確認。あなたの活動と敵との遭遇を追跡。'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: '惑星防衛',
|
||||||
|
message: '防衛施設を建設して攻撃から惑星を守りましょう。シールドとタレットで侵略者を威嚇!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: '士官',
|
||||||
|
message: '士官を雇用して様々なボーナスを獲得!司令官は建設を加速、地質学者は資源生産を向上、提督は艦隊能力を強化。'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: '戦闘シミュレーター',
|
||||||
|
message: '攻撃前に戦闘結果をシミュレート。双方の艦隊と技術レベルを入力して、勝敗と損失を予測。'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: '設定',
|
||||||
|
message: 'ここでゲームデータの管理、通知設定、セーブのエクスポート/インポートができます。定期的にバックアップを!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM管理パネル',
|
||||||
|
message: 'GMモードでは資源、建物、技術レベルを素早く変更できます。テストや完全なゲームコンテンツの体験に使用。'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: '별들을 정복하라',
|
||||||
|
startGame: '게임 시작',
|
||||||
|
privacyAgreement: '개인정보 처리방침',
|
||||||
|
privacyAgreementDesc: '게임을 시작하기 전에 개인정보 처리방침을 읽고 동의해 주세요.',
|
||||||
|
agreeToPrivacy: '읽었으며 동의합니다',
|
||||||
|
viewFullPolicy: '전체 정책 보기',
|
||||||
|
agreeAndStart: '동의 후 시작'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: '확인',
|
confirm: '확인',
|
||||||
cancel: '취소',
|
cancel: '취소',
|
||||||
@@ -8,6 +17,7 @@ export default {
|
|||||||
close: '닫기',
|
close: '닫기',
|
||||||
back: '돌아가기',
|
back: '돌아가기',
|
||||||
next: '다음',
|
next: '다음',
|
||||||
|
gotIt: '',
|
||||||
previous: '이전',
|
previous: '이전',
|
||||||
submit: '제출',
|
submit: '제출',
|
||||||
reset: '초기화',
|
reset: '초기화',
|
||||||
@@ -31,10 +41,12 @@ export default {
|
|||||||
goToBuildings: '건물로 이동',
|
goToBuildings: '건물로 이동',
|
||||||
locked: '잠김',
|
locked: '잠김',
|
||||||
viewRequirements: '요구사항 보기',
|
viewRequirements: '요구사항 보기',
|
||||||
|
requirements: '요구사항',
|
||||||
requirementsNotMet: '요구사항 미충족',
|
requirementsNotMet: '요구사항 미충족',
|
||||||
current: '현재',
|
current: '현재',
|
||||||
level: '레벨',
|
level: '레벨',
|
||||||
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.'
|
gmModeActivated: 'GM 모드가 활성화되었습니다! 탐색 메뉴를 확인하세요.',
|
||||||
|
view: '보기'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: '전제 조건 미충족',
|
requirementsNotMet: '전제 조건 미충족',
|
||||||
@@ -56,7 +68,8 @@ export default {
|
|||||||
buildingLevelZero: '건물 레벨이 0이므로 철거할 수 없습니다',
|
buildingLevelZero: '건물 레벨이 0이므로 철거할 수 없습니다',
|
||||||
researchQueueFull: '연구 대기열 가득 참',
|
researchQueueFull: '연구 대기열 가득 참',
|
||||||
moonExists: '위성이 이미 존재합니다',
|
moonExists: '위성이 이미 존재합니다',
|
||||||
insufficientDebris: '잔해장 부족'
|
insufficientDebris: '잔해장 부족',
|
||||||
|
launchFailed: '발사 실패'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: '개요',
|
overview: '개요',
|
||||||
@@ -111,7 +124,13 @@ export default {
|
|||||||
homePlanet: '모행성',
|
homePlanet: '모행성',
|
||||||
planetPrefix: '행성',
|
planetPrefix: '행성',
|
||||||
moonSuffix: '의 위성',
|
moonSuffix: '의 위성',
|
||||||
colonyPrefix: '식민지'
|
colonyPrefix: '식민지',
|
||||||
|
renamePlanet: '행성 이름 변경',
|
||||||
|
renamePlanetTitle: '행성 이름 변경',
|
||||||
|
newPlanetName: '새 이름',
|
||||||
|
planetNamePlaceholder: '새 행성 이름 입력',
|
||||||
|
rename: '이름 변경',
|
||||||
|
renameSuccess: '행성 이름이 {name}(으)로 변경되었습니다'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: '총 점수'
|
points: '총 점수'
|
||||||
@@ -125,6 +144,7 @@ export default {
|
|||||||
roboticsFactory: '로봇 공장',
|
roboticsFactory: '로봇 공장',
|
||||||
naniteFactory: '나노 공장',
|
naniteFactory: '나노 공장',
|
||||||
shipyard: '조선소',
|
shipyard: '조선소',
|
||||||
|
hangar: '격납고',
|
||||||
researchLab: '연구소',
|
researchLab: '연구소',
|
||||||
metalStorage: '금속 창고',
|
metalStorage: '금속 창고',
|
||||||
crystalStorage: '크리스탈 창고',
|
crystalStorage: '크리스탈 창고',
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
jumpGate: '점프 게이트',
|
jumpGate: '점프 게이트',
|
||||||
planetDestroyerFactory: '행성 파괴 공장',
|
planetDestroyerFactory: '행성 파괴 공장',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
production: '생산량',
|
production: '생산량',
|
||||||
consumption: '소비',
|
consumption: '소비',
|
||||||
totalCost: '총 비용',
|
totalCost: '총 비용',
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
roboticsFactory: '건설 속도 향상',
|
roboticsFactory: '건설 속도 향상',
|
||||||
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
naniteFactory: '건설 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
||||||
shipyard: '함선 건조',
|
shipyard: '함선 건조',
|
||||||
|
hangar: '함대 저장 용량 확장 전용 시설, 행성 전문화 지원',
|
||||||
researchLab: '기술 연구',
|
researchLab: '기술 연구',
|
||||||
metalStorage: '금속 저장 용량 증가',
|
metalStorage: '금속 저장 용량 증가',
|
||||||
crystalStorage: '크리스탈 저장 용량 증가',
|
crystalStorage: '크리스탈 저장 용량 증가',
|
||||||
@@ -199,10 +221,10 @@ export default {
|
|||||||
lightFighter: '기본 전투 유닛',
|
lightFighter: '기본 전투 유닛',
|
||||||
heavyFighter: '중장갑 전투기',
|
heavyFighter: '중장갑 전투기',
|
||||||
cruiser: '중형 전함, 공격과 방어 균형',
|
cruiser: '중형 전함, 공격과 방어 균형',
|
||||||
battleship: '강력한 전함',
|
battleship: '주력 중전함, 강력한 화력과 높은 방어력 보유',
|
||||||
battlecruiser: '빠르고 강력한 전투함, 전함 공격에 탁월',
|
battlecruiser: '빠르고 강력한 전투함, 전함 공격에 탁월',
|
||||||
bomber: '방어 시설 공격 전문 함선',
|
bomber: '방어 시설 공격 전문 함선',
|
||||||
destroyer: '대형 함선 파괴 전문 헌터',
|
destroyer: '대형 함선 전문 격파함, 높은 화력이지만 방어력 낮음',
|
||||||
smallCargo: '소량의 자원 운송',
|
smallCargo: '소량의 자원 운송',
|
||||||
largeCargo: '대량의 자원 운송',
|
largeCargo: '대량의 자원 운송',
|
||||||
colonyShip: '새로운 행성 식민에 사용',
|
colonyShip: '새로운 행성 식민에 사용',
|
||||||
@@ -278,8 +300,9 @@ export default {
|
|||||||
ionTechnology: '이온 무기 기술',
|
ionTechnology: '이온 무기 기술',
|
||||||
hyperspaceTechnology: '초공간 점프 기술',
|
hyperspaceTechnology: '초공간 점프 기술',
|
||||||
plasmaTechnology: '플라즈마 무기 기술',
|
plasmaTechnology: '플라즈마 무기 기술',
|
||||||
computerTechnology: '연구 대기열 수 증가, 레벨당 +1 (최대 10레벨)',
|
computerTechnology: '연구 대기열 및 함대 임무 슬롯 증가, 레벨당 +1 대기열 +1 슬롯 (최대 10레벨)',
|
||||||
espionageTechnology: '스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1',
|
espionageTechnology:
|
||||||
|
'스파이 탐사기 효과 향상, 레벨당 정찰 깊이 +1. 정찰 레벨 = 내 레벨 - 상대 레벨 + 탐사기 수/5. ≥-1 함대 표시, ≥1 방어 표시, ≥3 건물 표시, ≥5 기술 표시',
|
||||||
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
weaponsTechnology: '함선과 방어의 공격력 레벨당 10% 증가',
|
||||||
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
shieldingTechnology: '함선과 방어의 실드 레벨당 10% 증가',
|
||||||
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
armourTechnology: '함선과 방어의 장갑 레벨당 10% 증가',
|
||||||
@@ -312,10 +335,13 @@ export default {
|
|||||||
darkMatterSpecialist: '암흑 물질 수집 효율 향상'
|
darkMatterSpecialist: '암흑 물질 수집 효율 향상'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: '진행 중인 작업',
|
||||||
|
empty: '활성 대기열 없음',
|
||||||
buildQueue: '건설 대기열',
|
buildQueue: '건설 대기열',
|
||||||
researchQueue: '연구 대기열',
|
researchQueue: '연구 대기열',
|
||||||
building: '건설 중',
|
building: '건설 중',
|
||||||
researching: '연구 중',
|
researching: '연구 중',
|
||||||
|
demolishing: '철거 중',
|
||||||
remaining: '남은 시간',
|
remaining: '남은 시간',
|
||||||
cancel: '취소',
|
cancel: '취소',
|
||||||
cancelBuild: '건설 취소',
|
cancelBuild: '건설 취소',
|
||||||
@@ -323,7 +349,14 @@ export default {
|
|||||||
confirmCancel: '취소하시겠습니까? 자원의 50%가 환불됩니다.',
|
confirmCancel: '취소하시겠습니까? 자원의 50%가 환불됩니다.',
|
||||||
level: '레벨',
|
level: '레벨',
|
||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeToLevel: '레벨로 업그레이드'
|
upgradeToLevel: '레벨로 업그레이드',
|
||||||
|
tabs: {
|
||||||
|
all: '전체',
|
||||||
|
buildings: '건물',
|
||||||
|
research: '연구',
|
||||||
|
ships: '함선',
|
||||||
|
defense: '방어'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: '행성 개요',
|
title: '행성 개요',
|
||||||
@@ -346,6 +379,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: '업그레이드 비용',
|
upgradeCost: '업그레이드 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '건설',
|
||||||
upgrade: '업그레이드',
|
upgrade: '업그레이드',
|
||||||
maxLevelReached: '최대 레벨 도달',
|
maxLevelReached: '최대 레벨 도달',
|
||||||
requirementsNotMet: '요구 사항 미충족',
|
requirementsNotMet: '요구 사항 미충족',
|
||||||
@@ -376,6 +410,7 @@ export default {
|
|||||||
fuelConsumption: '연료 소비',
|
fuelConsumption: '연료 소비',
|
||||||
buildCost: '건설 비용',
|
buildCost: '건설 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
perUnit: '단위당',
|
perUnit: '단위당',
|
||||||
batchCalculator: '일괄 계산기',
|
batchCalculator: '일괄 계산기',
|
||||||
quantity: '수량',
|
quantity: '수량',
|
||||||
@@ -406,6 +441,7 @@ export default {
|
|||||||
armor: '장갑',
|
armor: '장갑',
|
||||||
buildCost: '건설 비용',
|
buildCost: '건설 비용',
|
||||||
buildTime: '건설 시간',
|
buildTime: '건설 시간',
|
||||||
|
build: '',
|
||||||
perUnit: '단위당',
|
perUnit: '단위당',
|
||||||
batchCalculator: '일괄 계산기',
|
batchCalculator: '일괄 계산기',
|
||||||
quantity: '수량',
|
quantity: '수량',
|
||||||
@@ -461,6 +497,7 @@ export default {
|
|||||||
colonize: '식민',
|
colonize: '식민',
|
||||||
spy: '정찰',
|
spy: '정찰',
|
||||||
deploy: '배치',
|
deploy: '배치',
|
||||||
|
expedition: '탐험',
|
||||||
recycle: '회수',
|
recycle: '회수',
|
||||||
transportResources: '자원 수송',
|
transportResources: '자원 수송',
|
||||||
totalCargoCapacity: '총 적재량',
|
totalCargoCapacity: '총 적재량',
|
||||||
@@ -574,17 +611,21 @@ export default {
|
|||||||
sendGift: '선물 보내기',
|
sendGift: '선물 보내기',
|
||||||
debris: '잔해',
|
debris: '잔해',
|
||||||
giftPlanetTitle: '선물 보내기',
|
giftPlanetTitle: '선물 보내기',
|
||||||
giftPlanetMessage: '행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.'
|
giftPlanetMessage:
|
||||||
|
'행성 [{coordinates}]에 자원을 선물로 보내시겠습니까?\n\n함대 페이지로 이동하여 수송선을 선택하고 자원을 적재하세요.',
|
||||||
|
npcPlanetName: '{name}의 행성'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: '메시지 센터',
|
title: '메시지 센터',
|
||||||
battles: '전투',
|
battles: '전투',
|
||||||
spy: '정찰',
|
spy: '정찰',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
battleReports: '전투 보고서',
|
battleReports: '전투 보고서',
|
||||||
spyReports: '정찰 보고서',
|
spyReports: '정찰 보고서',
|
||||||
noBattleReports: '전투 보고서 없음',
|
noBattleReports: '전투 보고서 없음',
|
||||||
noSpyReports: '정찰 보고서 없음',
|
noSpyReports: '정찰 보고서 없음',
|
||||||
|
noDiplomaticReports: '',
|
||||||
battleReport: '전투 보고서',
|
battleReport: '전투 보고서',
|
||||||
spyReport: '정찰 보고서',
|
spyReport: '정찰 보고서',
|
||||||
victory: '승리',
|
victory: '승리',
|
||||||
@@ -606,6 +647,7 @@ export default {
|
|||||||
targetPlanet: '목표 행성',
|
targetPlanet: '목표 행성',
|
||||||
attackerRemaining: '공격자 잔여',
|
attackerRemaining: '공격자 잔여',
|
||||||
defenderRemaining: '방어자 잔여',
|
defenderRemaining: '방어자 잔여',
|
||||||
|
allDestroyed: '모두 파괴됨',
|
||||||
moonChance: '위성 생성 확률',
|
moonChance: '위성 생성 확률',
|
||||||
showRoundDetails: '라운드 상세 표시',
|
showRoundDetails: '라운드 상세 표시',
|
||||||
hideRoundDetails: '라운드 상세 숨기기',
|
hideRoundDetails: '라운드 상세 숨기기',
|
||||||
@@ -640,7 +682,48 @@ export default {
|
|||||||
hostile: '상대방이 적대적이어서 선물을 받지 않습니다',
|
hostile: '상대방이 적대적이어서 선물을 받지 않습니다',
|
||||||
neutral_distrust: '상대방이 당신을 신뢰하지 않습니다',
|
neutral_distrust: '상대방이 당신을 신뢰하지 않습니다',
|
||||||
polite_decline: '정중하게 거절했습니다'
|
polite_decline: '정중하게 거절했습니다'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: '',
|
||||||
|
spyDetected: '',
|
||||||
|
detectionResult: '',
|
||||||
|
detectionSuccess: '',
|
||||||
|
spiedNotificationMessage: '',
|
||||||
|
spiedNotificationTip: '',
|
||||||
|
viewInGalaxy: '',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: '',
|
||||||
|
missionSuccess: '',
|
||||||
|
missionFailed: '',
|
||||||
|
origin: '',
|
||||||
|
destination: '',
|
||||||
|
missionDetails: '',
|
||||||
|
transportedResources: '',
|
||||||
|
recycledResources: '',
|
||||||
|
remainingDebris: '',
|
||||||
|
newPlanet: '',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: '',
|
||||||
|
activityType: {
|
||||||
|
recycle: ''
|
||||||
|
},
|
||||||
|
activityLocation: '',
|
||||||
|
position: '',
|
||||||
|
nearPlanet: '',
|
||||||
|
activityDescription: '',
|
||||||
|
npcActivityMessage: '',
|
||||||
|
arrivalTime: '',
|
||||||
|
npcActivityTip: '',
|
||||||
|
clearMessages: '메시지 삭제',
|
||||||
|
clearMessageTypes: '삭제할 메시지 유형 선택',
|
||||||
|
clearBattleReports: '전투 보고서',
|
||||||
|
clearSpyReports: '정찰 보고서',
|
||||||
|
clearSpiedNotifications: '정찰 알림',
|
||||||
|
clearMissionReports: '임무 보고서',
|
||||||
|
clearNPCActivity: 'NPC 활동',
|
||||||
|
clearGiftNotifications: '선물 알림',
|
||||||
|
clearGiftRejected: '거절된 선물',
|
||||||
|
clearNow: '지금 삭제'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
|
transportSuccess: '수송 임무가 성공적으로 완료되었습니다',
|
||||||
@@ -652,7 +735,19 @@ export default {
|
|||||||
recycleSuccess: '회수 임무가 성공적으로 완료되었습니다',
|
recycleSuccess: '회수 임무가 성공적으로 완료되었습니다',
|
||||||
recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다',
|
recycleFailed: '회수 임무 실패, 목표 위치에 잔해가 없습니다',
|
||||||
destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다',
|
destroySuccess: '행성 파괴 임무가 성공적으로 실행되었습니다',
|
||||||
destroyFailed: '행성 파괴 임무 실패'
|
destroyFailed: '행성 파괴 임무 실패',
|
||||||
|
missileAttackSuccess: '미사일 공격 성공',
|
||||||
|
missileAttackFailed: '미사일 공격 실패, 목표 행성이 존재하지 않습니다',
|
||||||
|
missileAttackIntercepted: '모든 미사일이 요격되었습니다',
|
||||||
|
hits: '명중',
|
||||||
|
expeditionResources: '탐험대가 자원을 발견했습니다!',
|
||||||
|
expeditionDarkMatter: '탐험대가 암흑 물질을 발견했습니다!',
|
||||||
|
expeditionFleet: '탐험대가 버려진 함선을 발견했습니다!',
|
||||||
|
expeditionPiratesAttack: '탐험대가 해적의 공격을 받아 일부 함선을 잃었습니다',
|
||||||
|
expeditionPiratesEscaped: '탐험대가 해적과 조우했지만 무사히 탈출했습니다',
|
||||||
|
expeditionAliensAttack: '탐험대가 외계인의 공격을 받아 일부 함선을 잃었습니다',
|
||||||
|
expeditionAliensEscaped: '탐험대가 외계인과 조우했지만 무사히 탈출했습니다',
|
||||||
|
expeditionNothing: '탐험대가 아무것도 발견하지 못했습니다'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: '전투 시뮬레이터',
|
title: '전투 시뮬레이터',
|
||||||
@@ -742,7 +837,27 @@ export default {
|
|||||||
buildDate: '빌드 날짜',
|
buildDate: '빌드 날짜',
|
||||||
community: '커뮤니티',
|
community: '커뮤니티',
|
||||||
github: 'GitHub 저장소',
|
github: 'GitHub 저장소',
|
||||||
qqGroup: 'QQ 그룹'
|
qqGroup: 'QQ 그룹',
|
||||||
|
privacyPolicy: '개인정보처리방침',
|
||||||
|
notifications: '알림 설정',
|
||||||
|
notificationsDesc: '게임 내 알림 관리',
|
||||||
|
notificationTypes: '알림 유형',
|
||||||
|
browserNotifications: '브라우저 알림',
|
||||||
|
inAppNotifications: '인앱 알림',
|
||||||
|
constructionComplete: '건설 완료',
|
||||||
|
researchComplete: '연구 완료',
|
||||||
|
browserPermission: '브라우저 알림 활성화',
|
||||||
|
permissionGranted: '권한 허용됨',
|
||||||
|
permissionDenied: '권한 거부됨/허용되지 않음',
|
||||||
|
inAppNotificationsDesc: '페이지 내 팝업으로 표시',
|
||||||
|
notificationsDisabled: '특정 알림을 설정하려면 위의 스위치 중 하나를 활성화하세요',
|
||||||
|
suppressInFocus: '페이지가 포커스될 때 브라우저 알림 숨기기',
|
||||||
|
expandTypes: '세부 정보 펼치기',
|
||||||
|
collapseTypes: '세부 정보 접기'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: '건설 완료',
|
||||||
|
researchComplete: '연구 완료'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM 제어판',
|
title: 'GM 제어판',
|
||||||
@@ -805,17 +920,42 @@ export default {
|
|||||||
completeQueuesSuccess: ''
|
completeQueuesSuccess: ''
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count}개의 적 함대 접근 중',
|
||||||
npcSpyIncoming: 'NPC 정찰 프로브 접근 중',
|
npcSpyIncoming: 'NPC 정찰 프로브 접근 중',
|
||||||
npcAttackIncoming: 'NPC 함대 공격 진행 중!',
|
npcAttackIncoming: 'NPC 함대 공격 진행 중!',
|
||||||
npcFleetIncoming: 'NPC 함대 접근 중',
|
npcFleetIncoming: 'NPC 함대 접근 중',
|
||||||
ships: '척',
|
ships: '척',
|
||||||
spiedBy: '정찰당함',
|
spiedBy: '정찰당함',
|
||||||
attackedBy: '공격당함',
|
attackedBy: '공격당함',
|
||||||
detectionSuccess: '정찰 발견됨',
|
detectionSuccess: '적의 정찰을 발견했습니다!',
|
||||||
detectionFailed: '정찰 미발견',
|
detectionFailed: '정찰 미발견',
|
||||||
npcSpiedYourPlanet: 'NPC가 당신의 행성을 정찰했습니다',
|
npcSpiedYourPlanet: 'NPC가 당신의 행성을 정찰했습니다',
|
||||||
npcAttackedYourPlanet: 'NPC가 당신의 행성을 공격했습니다'
|
npcAttackedYourPlanet: 'NPC가 당신의 행성을 공격했습니다'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: '적 경보',
|
||||||
|
markAllRead: '모두 읽음',
|
||||||
|
noAlerts: '경보 없음',
|
||||||
|
fleetSize: '함대 규모',
|
||||||
|
ships: '척',
|
||||||
|
viewFleet: '함대 보기',
|
||||||
|
alertDetails: '경보 상세',
|
||||||
|
targetInfo: '목표 정보',
|
||||||
|
arrivalTime: '도착 시간',
|
||||||
|
countdown: '카운트다운',
|
||||||
|
viewMessages: '메시지 보기',
|
||||||
|
arrived: '도착함',
|
||||||
|
missionType: {
|
||||||
|
spy: '정찰',
|
||||||
|
attack: '공격',
|
||||||
|
unknown: '알 수 없음'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: '적 정찰 접근 중!',
|
||||||
|
attack: '적 공격 접근 중!',
|
||||||
|
unknown: '적 함대 접근 중!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: '외교',
|
title: '외교',
|
||||||
description: 'NPC와의 외교 관계 관리',
|
description: 'NPC와의 외교 관계 관리',
|
||||||
@@ -832,6 +972,10 @@ export default {
|
|||||||
recentEvents: '최근 이벤트',
|
recentEvents: '최근 이벤트',
|
||||||
recentEventsDescription: '최근 외교 활동 로그',
|
recentEventsDescription: '최근 외교 활동 로그',
|
||||||
ago: '전',
|
ago: '전',
|
||||||
|
notifications: '',
|
||||||
|
markAllRead: '',
|
||||||
|
noReports: '',
|
||||||
|
viewAll: '',
|
||||||
status: {
|
status: {
|
||||||
friendly: '우호적',
|
friendly: '우호적',
|
||||||
neutral: '중립',
|
neutral: '중립',
|
||||||
@@ -844,9 +988,30 @@ export default {
|
|||||||
more: '더보기',
|
more: '더보기',
|
||||||
actions: {
|
actions: {
|
||||||
gift: '선물 보내기',
|
gift: '선물 보내기',
|
||||||
viewPlanets: '행성 보기'
|
viewPlanets: '행성 보기',
|
||||||
|
addNote: '메모 추가',
|
||||||
|
editNote: '메모 편집'
|
||||||
},
|
},
|
||||||
|
note: '메모',
|
||||||
|
notePlaceholder: '메모 입력...',
|
||||||
|
noteEmpty: '메모 없음',
|
||||||
lastEvent: '최근 이벤트',
|
lastEvent: '최근 이벤트',
|
||||||
|
reportDetails: '외교 보고서 상세',
|
||||||
|
eventDescription: '이벤트 설명',
|
||||||
|
reputationChange: '평판 변화',
|
||||||
|
before: '이전',
|
||||||
|
after: '이후',
|
||||||
|
statusChange: '관계 상태 변화',
|
||||||
|
viewDiplomacy: '외교 페이지 보기',
|
||||||
|
eventType: {
|
||||||
|
gift: '자원을 선물함',
|
||||||
|
attack: '공격을 시작함',
|
||||||
|
allyAttacked: '동맹을 공격함',
|
||||||
|
spy: '정찰을 수행함',
|
||||||
|
stealDebris: '잔해를 약탈함',
|
||||||
|
destroyPlanet: '행성을 파괴함',
|
||||||
|
unknown: '알 수 없는 이벤트'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: '선물 전송',
|
gift: '선물 전송',
|
||||||
attack: '공격',
|
attack: '공격',
|
||||||
@@ -875,12 +1040,63 @@ export default {
|
|||||||
receivedGiftFromNpc: '{npcName}로부터 선물을 받았습니다',
|
receivedGiftFromNpc: '{npcName}로부터 선물을 받았습니다',
|
||||||
acceptedGiftFromNpc: '{npcName}의 선물을 받았습니다: {metal}M {crystal}C {deuterium}D',
|
acceptedGiftFromNpc: '{npcName}의 선물을 받았습니다: {metal}M {crystal}C {deuterium}D',
|
||||||
playerRejectedGift: '플레이어가 선물을 거부했습니다',
|
playerRejectedGift: '플레이어가 선물을 거부했습니다',
|
||||||
rejectedGiftFromNpc: '{npcName}의 선물을 거부했습니다. 평판 {reputation}'
|
rejectedGiftFromNpc: '{npcName}의 선물을 거부했습니다. 평판 {reputation}',
|
||||||
|
destroyedNpcPlanet: '{npcName}의 {planetName}을(를) 파괴했습니다',
|
||||||
|
playerDestroyedPlanet: '플레이어가 {planetName}을(를) 파괴했습니다',
|
||||||
|
youDestroyedNpcPlanet: '당신은 {npcName}의 {planetName}을(를) 파괴했습니다. 평판 {reputation}',
|
||||||
|
playerDestroyedAllyPlanet: '플레이어가 동맹 {allyName}의 {planetName}을(를) 파괴했습니다',
|
||||||
|
allyOutraged: '{allyName}은(는) 당신이 동맹 {targetName}의 {planetName}을(를) 파괴한 것에 분노하고 있습니다',
|
||||||
|
npcEliminated: 'NPC {npcName}이(가) 완전히 제거되었습니다',
|
||||||
|
npcEliminatedMessage: '당신은 {npcName}의 모든 행성을 파괴했습니다! 이 세력은 완전히 소멸되었습니다.'
|
||||||
|
},
|
||||||
|
searchPlaceholder: 'NPC 이름 검색...',
|
||||||
|
viewMode: {
|
||||||
|
card: '카드',
|
||||||
|
list: '목록'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC 상태 진단',
|
||||||
|
title: 'NPC 상태 진단',
|
||||||
|
description: '플레이어 점수: {points}, 정찰 간격: {spyInterval}분, 공격 간격: {attackInterval}분, 공격 확률: {attackProb}%',
|
||||||
|
noData: 'NPC 데이터 없음',
|
||||||
|
difficulty: '난이도',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: '쉬움',
|
||||||
|
medium: '보통',
|
||||||
|
hard: '어려움'
|
||||||
|
},
|
||||||
|
reputation: '평판',
|
||||||
|
spyProbes: '정찰기 수',
|
||||||
|
fleetPower: '함대 전력',
|
||||||
|
canSpy: '정찰 가능',
|
||||||
|
canAttack: '공격 가능',
|
||||||
|
attackProbability: '공격 확률',
|
||||||
|
nextSpy: '다음 정찰',
|
||||||
|
nextAttack: '다음 공격',
|
||||||
|
yes: '예',
|
||||||
|
no: '아니오',
|
||||||
|
timeFormat: '{min}분 {sec}초',
|
||||||
|
anytime: '언제든지',
|
||||||
|
statusExplanation: '상태 설명',
|
||||||
|
noRelation: '관계 없음',
|
||||||
|
noRelationNeutral: '관계 없음 (중립)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: '우호적 관계, 행동하지 않음',
|
||||||
|
neutralNoAction: '중립적 관계, 행동하지 않음',
|
||||||
|
hostileWillAct: '적대적 관계, 행동할 수 있음',
|
||||||
|
noRelationNeutral: '외교 관계 없음, 중립으로 취급',
|
||||||
|
insufficientProbes: '정찰기 부족 (현재: {current}, 필요: {required})',
|
||||||
|
noFleet: '전투 함대 없음',
|
||||||
|
spyCooldown: '정찰 쿨다운 중 ({min}분 {sec}초)',
|
||||||
|
attackCooldown: '공격 쿨다운 중 ({min}분 {sec}초)',
|
||||||
|
notSpiedYet: '아직 정찰하지 않음, 먼저 정찰 필요'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: '이전',
|
previous: '이전',
|
||||||
next: '다음',
|
next: '다음',
|
||||||
|
gotIt: '',
|
||||||
first: '처음',
|
first: '처음',
|
||||||
last: '마지막',
|
last: '마지막',
|
||||||
page: '{page}페이지'
|
page: '{page}페이지'
|
||||||
@@ -889,5 +1105,148 @@ export default {
|
|||||||
title: '페이지를 찾을 수 없습니다',
|
title: '페이지를 찾을 수 없습니다',
|
||||||
description: '죄송합니다. 찾으시는 페이지가 존재하지 않습니다',
|
description: '죄송합니다. 찾으시는 페이지가 존재하지 않습니다',
|
||||||
goHome: '홈으로 이동'
|
goHome: '홈으로 이동'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: '개인정보처리방침',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: '소개',
|
||||||
|
content:
|
||||||
|
'이 개인정보처리방침은 OGame-Vue-Ts가 귀하의 데이터를 어떻게 처리하는지 설명합니다. 우리는 귀하의 개인정보 보호에 전념하며, 이 게임은 사용자 개인정보를 완전히 존중하도록 설계되었습니다.'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: '데이터 수집',
|
||||||
|
content: '이 게임은 다음 데이터만 브라우저에 로컬로 수집하고 저장합니다:',
|
||||||
|
items: {
|
||||||
|
gameProgress: '게임 진행 상황 (건물 레벨, 함대, 자원 등)',
|
||||||
|
settings: '게임 설정 (알림 설정, 표시 옵션 등)',
|
||||||
|
language: '언어 설정'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: '데이터 저장',
|
||||||
|
content:
|
||||||
|
'모든 데이터는 브라우저의 로컬 스토리지(localStorage)에 저장됩니다. 이는 귀하의 데이터가 항상 귀하의 장치에 남아 있으며, 우리는 귀하의 게임 데이터에 접근, 조회 또는 수집할 수 없음을 의미합니다.'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: '서버 통신 없음',
|
||||||
|
content:
|
||||||
|
'이 게임은 완전히 오프라인인 싱글 플레이어 게임입니다. 업데이트 확인 기능(GitHub에서 버전 정보를 가져옴)을 제외하고 게임은 어떤 서버와도 통신하지 않습니다. 귀하의 게임 데이터는 절대로 장치를 떠나지 않습니다.'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: '제3자 서비스',
|
||||||
|
content:
|
||||||
|
'이 게임은 방문자 통계 및 트래픽 소스를 추적하기 위해 제3자 분석 서비스를 사용합니다. 이를 통해 사용 패턴을 이해하고 게임 경험을 개선할 수 있습니다. 이 분석 데이터는 익명이며 개인 식별 정보를 포함하지 않습니다. 광고 서비스나 기타 상업적 추적 도구는 사용하지 않습니다.'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: '데이터 제어',
|
||||||
|
content: '귀하는 데이터를 완전히 제어할 수 있습니다:',
|
||||||
|
items: {
|
||||||
|
export: '언제든지 게임 데이터를 내보낼 수 있습니다',
|
||||||
|
import: '백업 파일에서 데이터를 가져올 수 있습니다',
|
||||||
|
delete: '브라우저 데이터를 지우거나 게임 내 "데이터 삭제" 기능을 사용하여 모든 데이터를 삭제할 수 있습니다'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: '문의하기',
|
||||||
|
content: '이 개인정보처리방침에 대한 질문이 있으시면 다음을 통해 문의해 주세요:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: '일',
|
||||||
|
hours: '시간',
|
||||||
|
minutes: '분',
|
||||||
|
seconds: '초'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
welcome: {
|
||||||
|
title: 'OGame에 오신 것을 환영합니다',
|
||||||
|
content: '환영합니다, 사령관! 기초부터 시작하여 우주 제국을 건설해 봅시다.'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: '태양광 발전소 건설',
|
||||||
|
content:
|
||||||
|
'먼저 태양광 발전소를 건설하세요! 행성에 에너지를 공급합니다. 에너지가 없으면 다른 자원 건물이 작동할 수 없습니다. 가장 중요한 첫 단계입니다.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '건설 대기열',
|
||||||
|
content:
|
||||||
|
'건물이 건설 대기열에 추가되었습니다. 오른쪽 상단의 대기열 아이콘을 클릭하면 진행 중인 모든 건설 및 연구 작업을 확인할 수 있습니다. 건설에는 시간이 걸리지만 대기하는 동안 계속 작업할 수 있습니다.'
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: 'OGame에 오신 것을 환영합니다 (모바일)',
|
||||||
|
content:
|
||||||
|
'환영합니다, 사령관! 터치스크린용으로 설계된 간소화된 튜토리얼입니다. 제국 건설을 시작할 수 있도록 핵심 기능을 빠르게 소개하겠습니다.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '건설 대기열',
|
||||||
|
content:
|
||||||
|
'오른쪽 상단의 대기열 아이콘을 클릭하여 건설 진행 상황을 확인하세요. 다른 페이지를 계속 탐색할 수 있으며, 건설은 백그라운드에서 진행됩니다.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: '닫기',
|
||||||
|
gotIt: '알겠습니다',
|
||||||
|
dontShowAgain: '다시 표시 안 함',
|
||||||
|
resetHints: '힌트 재설정',
|
||||||
|
resetHintsDesc: '모든 힌트 다시 표시',
|
||||||
|
hintsEnabled: '힌트 활성화',
|
||||||
|
hintsEnabledDesc: '페이지 방문 시 도움말 힌트 표시',
|
||||||
|
overview: {
|
||||||
|
title: '행성 개요',
|
||||||
|
message: '여기서 행성 자원, 함대 상태, 생산 세부 정보를 확인할 수 있습니다. 진행 상황을 모니터링하려면 자주 확인하세요!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: '건물',
|
||||||
|
message:
|
||||||
|
'여기서 구조물을 건설하고 업그레이드합니다. 태양광 발전소로 에너지를 확보한 다음 자원 광산을 건설하세요. 팁: 로봇 공장이 건설 속도를 높입니다!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: '연구소',
|
||||||
|
message: '기술을 연구하여 새로운 함선을 해제하고, 전투력을 향상시키고, 문명을 발전시키세요. 에너지 기술이 좋은 시작점입니다!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: '조선소',
|
||||||
|
message: '함선을 건조하여 탐험, 자원 운송, 제국 방어에 활용하세요. 화물선은 행성 간 자원을 운반합니다.'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: '함대 사령부',
|
||||||
|
message: '함선을 임무에 파견하세요: 적 공격, 자원 수송, 새 행성 식민지화, 또는 잔해장 탐색.'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: '은하 지도',
|
||||||
|
message: '은하를 탐색하여 식민지화할 빈 행성, 수확할 잔해장, 공격할 적을 찾으세요. 먼저 정찰 탐침을 사용하세요!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: '외교',
|
||||||
|
message: 'NPC와의 관계를 관리하세요. 선물을 보내 평판을 높이거나 적대적 공격에 직면하세요. 적의 동맹도 적대적으로 변할 수 있습니다!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: '메시지',
|
||||||
|
message: '여기서 전투 보고서, 정찰 보고서, 외교 알림을 확인하세요. 활동과 적 조우를 추적하세요.'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: '행성 방어',
|
||||||
|
message: '방어 구조물을 건설하여 공격으로부터 행성을 보호하세요. 방패와 포탑이 침입자를 억제합니다!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: '장교',
|
||||||
|
message: '장교를 고용하여 다양한 보너스를 획득하세요! 사령관은 건설 가속, 지질학자는 자원 생산 증가, 제독은 함대 능력 강화.'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: '전투 시뮬레이터',
|
||||||
|
message: '공격 전에 전투 결과를 시뮬레이션하세요. 양측 함대와 기술 레벨을 입력하여 승패와 손실을 예측.'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: '설정',
|
||||||
|
message: '여기서 게임 데이터 관리, 알림 설정, 저장 내보내기/가져오기가 가능합니다. 정기적으로 백업하세요!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM 관리 패널',
|
||||||
|
message: 'GM 모드에서는 자원, 건물, 기술 레벨을 빠르게 수정할 수 있습니다. 테스트나 전체 게임 콘텐츠 체험에 사용.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: 'Покори звёзды',
|
||||||
|
startGame: 'Начать игру',
|
||||||
|
privacyAgreement: 'Политика конфиденциальности',
|
||||||
|
privacyAgreementDesc: 'Пожалуйста, прочитайте и примите нашу политику конфиденциальности перед началом игры.',
|
||||||
|
agreeToPrivacy: 'Я прочитал и согласен с',
|
||||||
|
viewFullPolicy: 'Просмотреть полную политику',
|
||||||
|
agreeAndStart: 'Согласиться и начать'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: 'Подтвердить',
|
confirm: 'Подтвердить',
|
||||||
cancel: 'Отмена',
|
cancel: 'Отмена',
|
||||||
@@ -8,6 +17,7 @@ export default {
|
|||||||
close: 'Закрыть',
|
close: 'Закрыть',
|
||||||
back: 'Назад',
|
back: 'Назад',
|
||||||
next: 'Далее',
|
next: 'Далее',
|
||||||
|
gotIt: '',
|
||||||
previous: 'Предыдущий',
|
previous: 'Предыдущий',
|
||||||
submit: 'Отправить',
|
submit: 'Отправить',
|
||||||
reset: 'Сбросить',
|
reset: 'Сбросить',
|
||||||
@@ -31,10 +41,12 @@ export default {
|
|||||||
goToBuildings: 'К зданиям',
|
goToBuildings: 'К зданиям',
|
||||||
locked: 'Заблокировано',
|
locked: 'Заблокировано',
|
||||||
viewRequirements: 'Просмотр требований',
|
viewRequirements: 'Просмотр требований',
|
||||||
|
requirements: 'Требования',
|
||||||
requirementsNotMet: 'Требования не выполнены',
|
requirementsNotMet: 'Требования не выполнены',
|
||||||
current: 'Текущий',
|
current: 'Текущий',
|
||||||
level: 'Уровень',
|
level: 'Уровень',
|
||||||
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.'
|
gmModeActivated: 'Режим GM активирован! Проверьте навигационное меню.',
|
||||||
|
view: 'Просмотр'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: 'Требования не выполнены',
|
requirementsNotMet: 'Требования не выполнены',
|
||||||
@@ -56,7 +68,8 @@ export default {
|
|||||||
buildingLevelZero: 'Уровень здания 0, нельзя снести',
|
buildingLevelZero: 'Уровень здания 0, нельзя снести',
|
||||||
researchQueueFull: 'Очередь исследований заполнена',
|
researchQueueFull: 'Очередь исследований заполнена',
|
||||||
moonExists: 'Луна уже существует',
|
moonExists: 'Луна уже существует',
|
||||||
insufficientDebris: 'Недостаточно обломков'
|
insufficientDebris: 'Недостаточно обломков',
|
||||||
|
launchFailed: 'Запуск не удался'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: 'Обзор',
|
overview: 'Обзор',
|
||||||
@@ -111,7 +124,13 @@ export default {
|
|||||||
homePlanet: 'Родная планета',
|
homePlanet: 'Родная планета',
|
||||||
planetPrefix: 'Планета',
|
planetPrefix: 'Планета',
|
||||||
moonSuffix: 'я луна',
|
moonSuffix: 'я луна',
|
||||||
colonyPrefix: 'Колония'
|
colonyPrefix: 'Колония',
|
||||||
|
renamePlanet: 'Переименовать планету',
|
||||||
|
renamePlanetTitle: 'Переименовать планету',
|
||||||
|
newPlanetName: 'Новое название',
|
||||||
|
planetNamePlaceholder: 'Введите новое название планеты',
|
||||||
|
rename: 'Переименовать',
|
||||||
|
renameSuccess: 'Планета переименована в {name}'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: 'Всего очков'
|
points: 'Всего очков'
|
||||||
@@ -125,6 +144,7 @@ export default {
|
|||||||
roboticsFactory: 'Фабрика роботов',
|
roboticsFactory: 'Фабрика роботов',
|
||||||
naniteFactory: 'Нанитная фабрика',
|
naniteFactory: 'Нанитная фабрика',
|
||||||
shipyard: 'Верфь',
|
shipyard: 'Верфь',
|
||||||
|
hangar: 'Ангар',
|
||||||
researchLab: 'Исследовательская лаборатория',
|
researchLab: 'Исследовательская лаборатория',
|
||||||
metalStorage: 'Хранилище металла',
|
metalStorage: 'Хранилище металла',
|
||||||
crystalStorage: 'Хранилище кристалла',
|
crystalStorage: 'Хранилище кристалла',
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
jumpGate: 'Прыжковые ворота',
|
jumpGate: 'Прыжковые ворота',
|
||||||
planetDestroyerFactory: 'Фабрика разрушителей планет',
|
planetDestroyerFactory: 'Фабрика разрушителей планет',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
production: 'Производство',
|
production: 'Производство',
|
||||||
consumption: 'Потребление',
|
consumption: 'Потребление',
|
||||||
totalCost: 'Общая стоимость',
|
totalCost: 'Общая стоимость',
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
roboticsFactory: 'Ускоряет скорость строительства',
|
roboticsFactory: 'Ускоряет скорость строительства',
|
||||||
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10 уровней)',
|
naniteFactory: 'Увеличивает вместимость очереди строительства, +1 за уровень (макс 10 уровней)',
|
||||||
shipyard: 'Строит корабли',
|
shipyard: 'Строит корабли',
|
||||||
|
hangar: 'Специализированное сооружение для расширения вместимости флота, поддерживает специализацию планет',
|
||||||
researchLab: 'Исследует технологии',
|
researchLab: 'Исследует технологии',
|
||||||
metalStorage: 'Увеличивает ёмкость хранилища металла',
|
metalStorage: 'Увеличивает ёмкость хранилища металла',
|
||||||
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
|
crystalStorage: 'Увеличивает ёмкость хранилища кристалла',
|
||||||
@@ -199,10 +221,10 @@ export default {
|
|||||||
lightFighter: 'Базовая боевая единица',
|
lightFighter: 'Базовая боевая единица',
|
||||||
heavyFighter: 'Тяжелобронированный истребитель',
|
heavyFighter: 'Тяжелобронированный истребитель',
|
||||||
cruiser: 'Средний боевой корабль, сбалансированная атака и защита',
|
cruiser: 'Средний боевой корабль, сбалансированная атака и защита',
|
||||||
battleship: 'Мощный боевой корабль',
|
battleship: 'Основной тяжёлый боевой корабль с мощной огневой мощью и высокой защитой',
|
||||||
battlecruiser: 'Быстрый мощный боевой корабль, отлично атакует линкоры',
|
battlecruiser: 'Быстрый мощный боевой корабль, отлично атакует линкоры',
|
||||||
bomber: 'Специализированный корабль для атаки оборонительных сооружений',
|
bomber: 'Специализированный корабль для атаки оборонительных сооружений',
|
||||||
destroyer: 'Охотник, специализирующийся на уничтожении крупных кораблей',
|
destroyer: 'Специализированный противокапитальный корабль с высокой огневой мощью, но низкой защитой',
|
||||||
smallCargo: 'Транспортирует небольшое количество ресурсов',
|
smallCargo: 'Транспортирует небольшое количество ресурсов',
|
||||||
largeCargo: 'Транспортирует большое количество ресурсов',
|
largeCargo: 'Транспортирует большое количество ресурсов',
|
||||||
colonyShip: 'Используется для колонизации новых планет',
|
colonyShip: 'Используется для колонизации новых планет',
|
||||||
@@ -278,8 +300,9 @@ export default {
|
|||||||
ionTechnology: 'Технология ионного оружия',
|
ionTechnology: 'Технология ионного оружия',
|
||||||
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
hyperspaceTechnology: 'Технология гиперпространственных прыжков',
|
||||||
plasmaTechnology: 'Технология плазменного оружия',
|
plasmaTechnology: 'Технология плазменного оружия',
|
||||||
computerTechnology: 'Увеличивает вместимость очереди исследований, +1 за уровень (макс 10 уровней)',
|
computerTechnology: 'Увеличивает очередь исследований и слоты флотских миссий, +1 очередь +1 слот за уровень (макс 10 уровней)',
|
||||||
espionageTechnology: 'Повышает эффективность зондов, +1 уровень шпионажа за уровень',
|
espionageTechnology:
|
||||||
|
'Повышает эффективность зондов, +1 уровень шпионажа за уровень. Уровень разведки = ваш уровень - уровень врага + зонды/5. ≥-1 показывает флот, ≥1 показывает оборону, ≥3 показывает здания, ≥5 показывает технологии',
|
||||||
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
weaponsTechnology: 'Увеличивает силу атаки кораблей и обороны на 10% за уровень',
|
||||||
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
shieldingTechnology: 'Увеличивает щиты кораблей и обороны на 10% за уровень',
|
||||||
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
armourTechnology: 'Увеличивает броню кораблей и обороны на 10% за уровень',
|
||||||
@@ -313,10 +336,13 @@ export default {
|
|||||||
darkMatterSpecialist: 'Улучшает эффективность сбора тёмной материи'
|
darkMatterSpecialist: 'Улучшает эффективность сбора тёмной материи'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: 'Активные задачи',
|
||||||
|
empty: 'Нет активных очередей',
|
||||||
buildQueue: 'Очередь строительства',
|
buildQueue: 'Очередь строительства',
|
||||||
researchQueue: 'Очередь исследований',
|
researchQueue: 'Очередь исследований',
|
||||||
building: 'Строится',
|
building: 'Строится',
|
||||||
researching: 'Исследуется',
|
researching: 'Исследуется',
|
||||||
|
demolishing: 'Сносится',
|
||||||
remaining: 'Осталось',
|
remaining: 'Осталось',
|
||||||
cancel: 'Отменить',
|
cancel: 'Отменить',
|
||||||
cancelBuild: 'Отменить строительство',
|
cancelBuild: 'Отменить строительство',
|
||||||
@@ -324,7 +350,14 @@ export default {
|
|||||||
confirmCancel: 'Вы уверены, что хотите отменить? 50% ресурсов будет возвращено.',
|
confirmCancel: 'Вы уверены, что хотите отменить? 50% ресурсов будет возвращено.',
|
||||||
level: 'Уровень',
|
level: 'Уровень',
|
||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeToLevel: 'Улучшить до уровня'
|
upgradeToLevel: 'Улучшить до уровня',
|
||||||
|
tabs: {
|
||||||
|
all: 'Все',
|
||||||
|
buildings: 'Здания',
|
||||||
|
research: 'Исследования',
|
||||||
|
ships: 'Корабли',
|
||||||
|
defense: 'Оборона'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: 'Обзор планеты',
|
title: 'Обзор планеты',
|
||||||
@@ -347,6 +380,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: 'Стоимость улучшения',
|
upgradeCost: 'Стоимость улучшения',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: 'Построить',
|
||||||
upgrade: 'Улучшить',
|
upgrade: 'Улучшить',
|
||||||
maxLevelReached: 'Достигнут максимальный уровень',
|
maxLevelReached: 'Достигнут максимальный уровень',
|
||||||
requirementsNotMet: 'Требования не выполнены',
|
requirementsNotMet: 'Требования не выполнены',
|
||||||
@@ -378,6 +412,7 @@ export default {
|
|||||||
fuelConsumption: 'Расход топлива',
|
fuelConsumption: 'Расход топлива',
|
||||||
buildCost: 'Стоимость постройки',
|
buildCost: 'Стоимость постройки',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
perUnit: 'За единицу',
|
perUnit: 'За единицу',
|
||||||
batchCalculator: 'Калькулятор партий',
|
batchCalculator: 'Калькулятор партий',
|
||||||
quantity: 'Количество',
|
quantity: 'Количество',
|
||||||
@@ -408,6 +443,7 @@ export default {
|
|||||||
armor: 'Броня',
|
armor: 'Броня',
|
||||||
buildCost: 'Стоимость постройки',
|
buildCost: 'Стоимость постройки',
|
||||||
buildTime: 'Время строительства',
|
buildTime: 'Время строительства',
|
||||||
|
build: '',
|
||||||
perUnit: 'За единицу',
|
perUnit: 'За единицу',
|
||||||
batchCalculator: 'Калькулятор партий',
|
batchCalculator: 'Калькулятор партий',
|
||||||
quantity: 'Количество',
|
quantity: 'Количество',
|
||||||
@@ -464,6 +500,7 @@ export default {
|
|||||||
colonize: 'Колонизация',
|
colonize: 'Колонизация',
|
||||||
spy: 'Разведка',
|
spy: 'Разведка',
|
||||||
deploy: 'Размещение',
|
deploy: 'Размещение',
|
||||||
|
expedition: 'Экспедиция',
|
||||||
recycle: 'Переработка',
|
recycle: 'Переработка',
|
||||||
transportResources: 'Транспортировка ресурсов',
|
transportResources: 'Транспортировка ресурсов',
|
||||||
totalCargoCapacity: 'Общая грузоподъёмность',
|
totalCargoCapacity: 'Общая грузоподъёмность',
|
||||||
@@ -581,17 +618,20 @@ export default {
|
|||||||
debris: 'Обломки',
|
debris: 'Обломки',
|
||||||
giftPlanetTitle: 'Отправить подарок',
|
giftPlanetTitle: 'Отправить подарок',
|
||||||
giftPlanetMessage:
|
giftPlanetMessage:
|
||||||
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.'
|
'Вы уверены, что хотите отправить ресурсы в подарок планете [{coordinates}]?\n\nПерейдите на страницу флота, чтобы выбрать транспортные корабли и загрузить ресурсы.',
|
||||||
|
npcPlanetName: 'Планета {name}'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: 'Сообщения',
|
title: 'Сообщения',
|
||||||
battles: 'Битвы',
|
battles: 'Битвы',
|
||||||
spy: 'Разведка',
|
spy: 'Разведка',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
battleReports: 'Отчёты о боях',
|
battleReports: 'Отчёты о боях',
|
||||||
spyReports: 'Отчёты разведки',
|
spyReports: 'Отчёты разведки',
|
||||||
noBattleReports: 'Нет отчётов о боях',
|
noBattleReports: 'Нет отчётов о боях',
|
||||||
noSpyReports: 'Нет отчётов разведки',
|
noSpyReports: 'Нет отчётов разведки',
|
||||||
|
noDiplomaticReports: '',
|
||||||
battleReport: 'Отчёт о бое',
|
battleReport: 'Отчёт о бое',
|
||||||
spyReport: 'Отчёт разведки',
|
spyReport: 'Отчёт разведки',
|
||||||
victory: 'Победа',
|
victory: 'Победа',
|
||||||
@@ -613,6 +653,7 @@ export default {
|
|||||||
targetPlanet: 'Целевая планета',
|
targetPlanet: 'Целевая планета',
|
||||||
attackerRemaining: 'Осталось у нападающего',
|
attackerRemaining: 'Осталось у нападающего',
|
||||||
defenderRemaining: 'Осталось у защитника',
|
defenderRemaining: 'Осталось у защитника',
|
||||||
|
allDestroyed: 'Всё уничтожено',
|
||||||
moonChance: 'Шанс появления луны',
|
moonChance: 'Шанс появления луны',
|
||||||
showRoundDetails: 'Показать детали раундов',
|
showRoundDetails: 'Показать детали раундов',
|
||||||
hideRoundDetails: 'Скрыть детали раундов',
|
hideRoundDetails: 'Скрыть детали раундов',
|
||||||
@@ -647,7 +688,48 @@ export default {
|
|||||||
hostile: 'Они враждебны и не принимают подарки',
|
hostile: 'Они враждебны и не принимают подарки',
|
||||||
neutral_distrust: 'Они вам не доверяют',
|
neutral_distrust: 'Они вам не доверяют',
|
||||||
polite_decline: 'Вежливо отказались'
|
polite_decline: 'Вежливо отказались'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: '',
|
||||||
|
spyDetected: '',
|
||||||
|
detectionResult: '',
|
||||||
|
detectionSuccess: '',
|
||||||
|
spiedNotificationMessage: '',
|
||||||
|
spiedNotificationTip: '',
|
||||||
|
viewInGalaxy: '',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: '',
|
||||||
|
missionSuccess: '',
|
||||||
|
missionFailed: '',
|
||||||
|
origin: '',
|
||||||
|
destination: '',
|
||||||
|
missionDetails: '',
|
||||||
|
transportedResources: '',
|
||||||
|
recycledResources: '',
|
||||||
|
remainingDebris: '',
|
||||||
|
newPlanet: '',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: '',
|
||||||
|
activityType: {
|
||||||
|
recycle: ''
|
||||||
|
},
|
||||||
|
activityLocation: '',
|
||||||
|
position: '',
|
||||||
|
nearPlanet: '',
|
||||||
|
activityDescription: '',
|
||||||
|
npcActivityMessage: '',
|
||||||
|
arrivalTime: '',
|
||||||
|
npcActivityTip: '',
|
||||||
|
clearMessages: 'Очистить сообщения',
|
||||||
|
clearMessageTypes: 'Выберите типы сообщений для очистки',
|
||||||
|
clearBattleReports: 'Боевые отчёты',
|
||||||
|
clearSpyReports: 'Разведывательные отчёты',
|
||||||
|
clearSpiedNotifications: 'Уведомления о разведке',
|
||||||
|
clearMissionReports: 'Отчёты о миссиях',
|
||||||
|
clearNPCActivity: 'Активность NPC',
|
||||||
|
clearGiftNotifications: 'Уведомления о подарках',
|
||||||
|
clearGiftRejected: 'Отклонённые подарки',
|
||||||
|
clearNow: 'Очистить сейчас'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: 'Миссия транспортировки успешно завершена',
|
transportSuccess: 'Миссия транспортировки успешно завершена',
|
||||||
@@ -659,7 +741,19 @@ export default {
|
|||||||
recycleSuccess: 'Миссия переработки успешно завершена',
|
recycleSuccess: 'Миссия переработки успешно завершена',
|
||||||
recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции',
|
recycleFailed: 'Миссия переработки провалена, нет обломков в целевой позиции',
|
||||||
destroySuccess: 'Миссия уничтожения планеты успешно выполнена',
|
destroySuccess: 'Миссия уничтожения планеты успешно выполнена',
|
||||||
destroyFailed: 'Миссия уничтожения планеты провалена'
|
destroyFailed: 'Миссия уничтожения планеты провалена',
|
||||||
|
expeditionResources: 'Экспедиция обнаружила ресурсы!',
|
||||||
|
expeditionDarkMatter: 'Экспедиция обнаружила тёмную материю!',
|
||||||
|
expeditionFleet: 'Экспедиция обнаружила заброшенные корабли!',
|
||||||
|
expeditionPiratesAttack: 'Экспедиция попала в засаду пиратов, часть кораблей потеряна',
|
||||||
|
expeditionPiratesEscaped: 'Экспедиция столкнулась с пиратами, но успела скрыться',
|
||||||
|
expeditionAliensAttack: 'Экспедиция подверглась атаке инопланетян, часть кораблей потеряна',
|
||||||
|
expeditionAliensEscaped: 'Экспедиция столкнулась с инопланетянами, но успела скрыться',
|
||||||
|
expeditionNothing: 'Экспедиция ничего не обнаружила',
|
||||||
|
missileAttackSuccess: 'Ракетная атака успешна!',
|
||||||
|
missileAttackFailed: 'Ракетная атака провалена',
|
||||||
|
missileAttackIntercepted: 'Ракеты перехвачены',
|
||||||
|
hits: 'попаданий'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: 'Симулятор боя',
|
title: 'Симулятор боя',
|
||||||
@@ -749,7 +843,27 @@ export default {
|
|||||||
buildDate: 'Дата сборки',
|
buildDate: 'Дата сборки',
|
||||||
community: 'Сообщество',
|
community: 'Сообщество',
|
||||||
github: 'Репозиторий GitHub',
|
github: 'Репозиторий GitHub',
|
||||||
qqGroup: 'Группа QQ'
|
qqGroup: 'Группа QQ',
|
||||||
|
privacyPolicy: 'Политика конфиденциальности',
|
||||||
|
notifications: 'Настройки уведомлений',
|
||||||
|
notificationsDesc: 'Управление внутриигровыми уведомлениями',
|
||||||
|
notificationTypes: 'Типы уведомлений',
|
||||||
|
browserNotifications: 'Уведомления браузера',
|
||||||
|
inAppNotifications: 'Внутриигровые уведомления',
|
||||||
|
constructionComplete: 'Строительство завершено',
|
||||||
|
researchComplete: 'Исследование завершено',
|
||||||
|
browserPermission: 'Включить уведомления браузера',
|
||||||
|
permissionGranted: 'Разрешение получено',
|
||||||
|
permissionDenied: 'Разрешение отклонено/не получено',
|
||||||
|
inAppNotificationsDesc: 'Показывать через всплывающие окна',
|
||||||
|
notificationsDisabled: 'Включите любой переключатель выше для настройки конкретных уведомлений',
|
||||||
|
suppressInFocus: 'Не отправлять уведомления браузера, когда страница в фокусе',
|
||||||
|
expandTypes: 'Развернуть детали',
|
||||||
|
collapseTypes: 'Свернуть детали'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: 'Строительство завершено',
|
||||||
|
researchComplete: 'Исследование завершено'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'Панель управления GM',
|
title: 'Панель управления GM',
|
||||||
@@ -801,7 +915,8 @@ export default {
|
|||||||
npcWillSpyAndAttack: '{npcName} проведет разведку через 5с и атакует через 10с',
|
npcWillSpyAndAttack: '{npcName} проведет разведку через 5с и атакует через 10с',
|
||||||
acceleratedMissions: 'Ускорено {count} миссий до 5 секунд',
|
acceleratedMissions: 'Ускорено {count} миссий до 5 секунд',
|
||||||
npcFleetInitialized: 'Флот {npcName} инициализирован',
|
npcFleetInitialized: 'Флот {npcName} инициализирован',
|
||||||
npcFleetDetails: '100 шпионских зондов\n500 легких истребителей\n300 тяжелых истребителей\n200 крейсеров\n100 линкоров\n50 бомбардировщиков\n30 эсминцев\n20 линейных крейсеров',
|
npcFleetDetails:
|
||||||
|
'100 шпионских зондов\n500 легких истребителей\n300 тяжелых истребителей\n200 крейсеров\n100 линкоров\n50 бомбардировщиков\n30 эсминцев\n20 линейных крейсеров',
|
||||||
dangerZone: 'Опасная зона',
|
dangerZone: 'Опасная зона',
|
||||||
dangerZoneDesc: 'Следующие операции необратимы',
|
dangerZoneDesc: 'Следующие операции необратимы',
|
||||||
resetGame: 'Сбросить игру',
|
resetGame: 'Сбросить игру',
|
||||||
@@ -812,17 +927,42 @@ export default {
|
|||||||
completeQueuesSuccess: ''
|
completeQueuesSuccess: ''
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count} вражеских флотов приближается',
|
||||||
npcSpyIncoming: 'Приближается шпионский зонд NPC',
|
npcSpyIncoming: 'Приближается шпионский зонд NPC',
|
||||||
npcAttackIncoming: 'Атака флота NPC приближается!',
|
npcAttackIncoming: 'Атака флота NPC приближается!',
|
||||||
npcFleetIncoming: 'Приближается флот NPC',
|
npcFleetIncoming: 'Приближается флот NPC',
|
||||||
ships: 'кораблей',
|
ships: 'кораблей',
|
||||||
spiedBy: 'Разведан',
|
spiedBy: 'Разведан',
|
||||||
attackedBy: 'Атакован',
|
attackedBy: 'Атакован',
|
||||||
detectionSuccess: 'Разведка обнаружена',
|
detectionSuccess: 'Вы обнаружили вражескую разведку!',
|
||||||
detectionFailed: 'Разведка не обнаружена',
|
detectionFailed: 'Разведка не обнаружена',
|
||||||
npcSpiedYourPlanet: 'NPC разведал вашу планету',
|
npcSpiedYourPlanet: 'NPC разведал вашу планету',
|
||||||
npcAttackedYourPlanet: 'NPC атаковал вашу планету'
|
npcAttackedYourPlanet: 'NPC атаковал вашу планету'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: 'Тревога',
|
||||||
|
markAllRead: 'Отметить прочитанным',
|
||||||
|
noAlerts: 'Нет тревог',
|
||||||
|
fleetSize: 'Размер флота',
|
||||||
|
ships: 'кораблей',
|
||||||
|
viewFleet: 'Просмотр флота',
|
||||||
|
alertDetails: 'Детали тревоги',
|
||||||
|
targetInfo: 'Информация о цели',
|
||||||
|
arrivalTime: 'Время прибытия',
|
||||||
|
countdown: 'Обратный отсчёт',
|
||||||
|
viewMessages: 'Просмотр сообщений',
|
||||||
|
arrived: 'Прибыл',
|
||||||
|
missionType: {
|
||||||
|
spy: 'Разведка',
|
||||||
|
attack: 'Атака',
|
||||||
|
unknown: 'Неизвестно'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: 'Вражеская разведка приближается!',
|
||||||
|
attack: 'Вражеская атака приближается!',
|
||||||
|
unknown: 'Вражеский флот приближается!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: 'Дипломатия',
|
title: 'Дипломатия',
|
||||||
description: 'Управление дипломатическими отношениями с NPC',
|
description: 'Управление дипломатическими отношениями с NPC',
|
||||||
@@ -839,6 +979,10 @@ export default {
|
|||||||
recentEvents: 'Недавние события',
|
recentEvents: 'Недавние события',
|
||||||
recentEventsDescription: 'Журнал последних дипломатических действий',
|
recentEventsDescription: 'Журнал последних дипломатических действий',
|
||||||
ago: 'назад',
|
ago: 'назад',
|
||||||
|
notifications: '',
|
||||||
|
markAllRead: '',
|
||||||
|
noReports: '',
|
||||||
|
viewAll: '',
|
||||||
status: {
|
status: {
|
||||||
friendly: 'Дружественный',
|
friendly: 'Дружественный',
|
||||||
neutral: 'Нейтральный',
|
neutral: 'Нейтральный',
|
||||||
@@ -851,9 +995,30 @@ export default {
|
|||||||
more: 'еще',
|
more: 'еще',
|
||||||
actions: {
|
actions: {
|
||||||
gift: 'Отправить подарок',
|
gift: 'Отправить подарок',
|
||||||
viewPlanets: 'Посмотреть планеты'
|
viewPlanets: 'Посмотреть планеты',
|
||||||
|
addNote: 'Добавить заметку',
|
||||||
|
editNote: 'Редактировать заметку'
|
||||||
},
|
},
|
||||||
|
note: 'Заметка',
|
||||||
|
notePlaceholder: 'Введите заметку...',
|
||||||
|
noteEmpty: 'Нет заметки',
|
||||||
lastEvent: 'Последнее событие',
|
lastEvent: 'Последнее событие',
|
||||||
|
reportDetails: 'Детали дипломатического отчёта',
|
||||||
|
eventDescription: 'Описание события',
|
||||||
|
reputationChange: 'Изменение репутации',
|
||||||
|
before: 'До',
|
||||||
|
after: 'После',
|
||||||
|
statusChange: 'Изменение статуса',
|
||||||
|
viewDiplomacy: 'Перейти к дипломатии',
|
||||||
|
eventType: {
|
||||||
|
gift: 'Подарил ресурсы',
|
||||||
|
attack: 'Провёл атаку',
|
||||||
|
allyAttacked: 'Атаковал союзника',
|
||||||
|
spy: 'Провёл разведку',
|
||||||
|
stealDebris: 'Украл обломки',
|
||||||
|
destroyPlanet: 'Уничтожил планету',
|
||||||
|
unknown: 'Неизвестное событие'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: 'Подарок отправлен',
|
gift: 'Подарок отправлен',
|
||||||
attack: 'Атака',
|
attack: 'Атака',
|
||||||
@@ -882,12 +1047,64 @@ export default {
|
|||||||
receivedGiftFromNpc: 'Получен подарок от {npcName}',
|
receivedGiftFromNpc: 'Получен подарок от {npcName}',
|
||||||
acceptedGiftFromNpc: 'Вы приняли подарок от {npcName}: {metal}M {crystal}C {deuterium}D',
|
acceptedGiftFromNpc: 'Вы приняли подарок от {npcName}: {metal}M {crystal}C {deuterium}D',
|
||||||
playerRejectedGift: 'Игрок отклонил подарок',
|
playerRejectedGift: 'Игрок отклонил подарок',
|
||||||
rejectedGiftFromNpc: 'Вы отклонили подарок от {npcName}. Репутация {reputation}'
|
rejectedGiftFromNpc: 'Вы отклонили подарок от {npcName}. Репутация {reputation}',
|
||||||
|
destroyedNpcPlanet: 'Уничтожена {planetName} игрока {npcName}',
|
||||||
|
playerDestroyedPlanet: 'Игрок уничтожил {planetName}',
|
||||||
|
youDestroyedNpcPlanet: 'Вы уничтожили {planetName} игрока {npcName}. Репутация {reputation}',
|
||||||
|
playerDestroyedAllyPlanet: 'Игрок уничтожил {planetName} союзника {allyName}',
|
||||||
|
allyOutraged: '{allyName} возмущен тем, что вы уничтожили {planetName} их союзника {targetName}',
|
||||||
|
npcEliminated: 'NPC {npcName} полностью уничтожен',
|
||||||
|
npcEliminatedMessage: 'Вы уничтожили все планеты {npcName}! Эта фракция полностью уничтожена.'
|
||||||
|
},
|
||||||
|
searchPlaceholder: 'Поиск NPC по имени...',
|
||||||
|
viewMode: {
|
||||||
|
card: 'Карточки',
|
||||||
|
list: 'Список'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'Диагностика NPC',
|
||||||
|
title: 'Диагностика состояния NPC',
|
||||||
|
description:
|
||||||
|
'Очки игрока: {points}, Интервал разведки: {spyInterval}мин, Интервал атаки: {attackInterval}мин, Вероятность атаки: {attackProb}%',
|
||||||
|
noData: 'Нет данных NPC',
|
||||||
|
difficulty: 'Сложность',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: 'Лёгкая',
|
||||||
|
medium: 'Средняя',
|
||||||
|
hard: 'Сложная'
|
||||||
|
},
|
||||||
|
reputation: 'Репутация',
|
||||||
|
spyProbes: 'Шпионские зонды',
|
||||||
|
fleetPower: 'Мощь флота',
|
||||||
|
canSpy: 'Может шпионить',
|
||||||
|
canAttack: 'Может атаковать',
|
||||||
|
attackProbability: 'Вероятность атаки',
|
||||||
|
nextSpy: 'Следующая разведка',
|
||||||
|
nextAttack: 'Следующая атака',
|
||||||
|
yes: 'Да',
|
||||||
|
no: 'Нет',
|
||||||
|
timeFormat: '{min}м {sec}с',
|
||||||
|
anytime: 'В любой момент',
|
||||||
|
statusExplanation: 'Объяснение статуса',
|
||||||
|
noRelation: 'Нет отношений',
|
||||||
|
noRelationNeutral: 'Нет отношений (Нейтральный)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: 'Дружественные отношения, не будет действовать',
|
||||||
|
neutralNoAction: 'Нейтральные отношения, не будет действовать',
|
||||||
|
hostileWillAct: 'Враждебные отношения, может действовать',
|
||||||
|
noRelationNeutral: 'Нет дипломатических отношений, считается нейтральным',
|
||||||
|
insufficientProbes: 'Недостаточно зондов (Текущее: {current}, Требуется: {required})',
|
||||||
|
noFleet: 'Нет боевого флота',
|
||||||
|
spyCooldown: 'Разведка на перезарядке ({min}м {sec}с)',
|
||||||
|
attackCooldown: 'Атака на перезарядке ({min}м {sec}с)',
|
||||||
|
notSpiedYet: 'Ещё не разведан, сначала нужна разведка'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: 'Предыдущая',
|
previous: 'Предыдущая',
|
||||||
next: 'Следующая',
|
next: 'Следующая',
|
||||||
|
gotIt: '',
|
||||||
first: 'Первая',
|
first: 'Первая',
|
||||||
last: 'Последняя',
|
last: 'Последняя',
|
||||||
page: 'Страница {page}'
|
page: 'Страница {page}'
|
||||||
@@ -896,5 +1113,157 @@ export default {
|
|||||||
title: 'Страница не найдена',
|
title: 'Страница не найдена',
|
||||||
description: 'Извините, страница, которую вы ищете, не существует',
|
description: 'Извините, страница, которую вы ищете, не существует',
|
||||||
goHome: 'На главную'
|
goHome: 'На главную'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: 'Политика конфиденциальности',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: 'Введение',
|
||||||
|
content:
|
||||||
|
'Эта политика конфиденциальности объясняет, как OGame-Vue-Ts обрабатывает ваши данные. Мы стремимся защищать вашу конфиденциальность, и эта игра разработана с полным уважением к приватности пользователей.'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: 'Сбор данных',
|
||||||
|
content: 'Эта игра собирает и хранит только следующие данные локально в вашем браузере:',
|
||||||
|
items: {
|
||||||
|
gameProgress: 'Прогресс игры (уровни зданий, флоты, ресурсы и т.д.)',
|
||||||
|
settings: 'Настройки игры (настройки уведомлений, параметры отображения и т.д.)',
|
||||||
|
language: 'Языковые настройки'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: 'Хранение данных',
|
||||||
|
content:
|
||||||
|
'Все данные хранятся в локальном хранилище вашего браузера (localStorage). Это означает, что ваши данные всегда остаются на вашем собственном устройстве, и мы не можем получить доступ, просматривать или собирать какие-либо ваши игровые данные.'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: 'Нет связи с сервером',
|
||||||
|
content:
|
||||||
|
'Эта игра является полностью офлайн одиночной игрой. За исключением функции проверки обновлений (которая получает информацию о версии с GitHub), игра не взаимодействует ни с каким сервером. Ваши игровые данные никогда не покидают ваше устройство.'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: 'Сторонние сервисы',
|
||||||
|
content:
|
||||||
|
'Эта игра использует сторонние аналитические сервисы для отслеживания статистики посещений и источников трафика, что помогает нам понять модели использования и улучшить игровой опыт. Эти аналитические данные являются анонимными и не содержат никакой персонально идентифицируемой информации. Мы не используем рекламные сервисы или другие коммерческие инструменты отслеживания.'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: 'Контроль данных',
|
||||||
|
content: 'Вы имеете полный контроль над своими данными:',
|
||||||
|
items: {
|
||||||
|
export: 'Вы можете экспортировать данные игры в любое время',
|
||||||
|
import: 'Вы можете импортировать данные из резервных файлов',
|
||||||
|
delete: 'Вы можете удалить все данные, очистив данные браузера или используя функцию "Очистить данные" в игре'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: 'Свяжитесь с нами',
|
||||||
|
content: 'Если у вас есть вопросы по поводу этой политики конфиденциальности, пожалуйста, свяжитесь с нами через:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: 'дней',
|
||||||
|
hours: 'часов',
|
||||||
|
minutes: 'минут',
|
||||||
|
seconds: 'секунд'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
welcome: {
|
||||||
|
title: 'Добро пожаловать в OGame',
|
||||||
|
content: 'Добро пожаловать, Командир! Давайте начнём с основ и построим вашу космическую империю.'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: 'Постройте солнечную электростанцию',
|
||||||
|
content:
|
||||||
|
'Сначала постройте солнечную электростанцию! Она обеспечивает энергией вашу планету. Без энергии другие ресурсные здания не могут функционировать. Это самый важный первый шаг.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Очередь строительства',
|
||||||
|
content:
|
||||||
|
'Ваше здание теперь в очереди строительства. Нажмите на значок очереди в правом верхнем углу, чтобы увидеть все текущие задачи строительства и исследований. Строительство занимает время, но вы можете продолжать работать во время ожидания.'
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: 'Добро пожаловать в OGame (Мобильная версия)',
|
||||||
|
content:
|
||||||
|
'Добро пожаловать, Командир! Это упрощённое руководство, разработанное для сенсорных экранов. Мы быстро рассмотрим основные функции, чтобы вы могли начать строить свою империю.'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: 'Очередь строительства',
|
||||||
|
content:
|
||||||
|
'Нажмите на значок очереди в правом верхнем углу, чтобы увидеть прогресс строительства. Вы можете продолжать просматривать другие страницы - строительство происходит в фоновом режиме.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: 'Закрыть',
|
||||||
|
gotIt: 'Понятно',
|
||||||
|
dontShowAgain: 'Больше не показывать',
|
||||||
|
resetHints: 'Сбросить подсказки',
|
||||||
|
resetHintsDesc: 'Показать все подсказки снова',
|
||||||
|
hintsEnabled: 'Включить подсказки',
|
||||||
|
hintsEnabledDesc: 'Показывать полезные подсказки при посещении страниц',
|
||||||
|
overview: {
|
||||||
|
title: 'Обзор планеты',
|
||||||
|
message:
|
||||||
|
'Здесь вы можете видеть ресурсы планеты, статус флота и детали производства. Регулярно проверяйте, чтобы отслеживать прогресс!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: 'Здания',
|
||||||
|
message:
|
||||||
|
'Стройте и улучшайте сооружения здесь. Начните с солнечной электростанции для энергии, затем ресурсные шахты. Совет: Фабрика роботов ускоряет строительство!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: 'Исследовательская лаборатория',
|
||||||
|
message:
|
||||||
|
'Исследуйте технологии, чтобы разблокировать новые корабли, улучшить боеспособность и развить цивилизацию. Энергетическая технология - отличное начало!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: 'Верфь',
|
||||||
|
message:
|
||||||
|
'Стройте корабли для исследования, транспортировки ресурсов и защиты империи. Грузовые корабли помогают перевозить ресурсы между планетами.'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: 'Командование флотом',
|
||||||
|
message:
|
||||||
|
'Отправляйте корабли на миссии: атакуйте врагов, транспортируйте ресурсы, колонизируйте новые планеты или исследуйте поля обломков.'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: 'Карта галактики',
|
||||||
|
message:
|
||||||
|
'Исследуйте галактику, чтобы найти пустые планеты для колонизации, поля обломков для сбора и врагов для атаки. Сначала используйте шпионские зонды!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: 'Дипломатия',
|
||||||
|
message:
|
||||||
|
'Управляйте отношениями с NPC. Отправляйте подарки для улучшения репутации или столкнитесь с враждебными атаками. Союзники ваших врагов тоже могут стать враждебными!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: 'Сообщения',
|
||||||
|
message:
|
||||||
|
'Просматривайте боевые отчёты, отчёты разведки и дипломатические уведомления. Отслеживайте свои действия и столкновения с врагами.'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: 'Планетарная оборона',
|
||||||
|
message: 'Стройте оборонительные сооружения для защиты планеты от атак. Щиты и турели могут отпугнуть захватчиков!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: 'Офицеры',
|
||||||
|
message:
|
||||||
|
'Нанимайте офицеров для получения бонусов! Командир ускоряет строительство, Геолог увеличивает добычу ресурсов, Адмирал усиливает флот.'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: 'Симулятор боя',
|
||||||
|
message: 'Симулируйте результаты боя перед атакой. Введите флоты и уровни технологий для прогноза победы, потерь и добычи.'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Настройки',
|
||||||
|
message: 'Управляйте игровыми данными, уведомлениями, импортом/экспортом сохранений. Регулярно создавайте резервные копии!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'Панель ГМ',
|
||||||
|
message:
|
||||||
|
'Режим ГМ позволяет быстро изменять ресурсы, здания и уровни технологий. Используйте для тестирования или полного доступа к контенту.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: '征服星辰大海',
|
||||||
|
startGame: '开始游戏',
|
||||||
|
privacyAgreement: '隐私协议',
|
||||||
|
privacyAgreementDesc: '开始游戏前,请阅读并同意我们的隐私协议。',
|
||||||
|
agreeToPrivacy: '我已阅读并同意',
|
||||||
|
viewFullPolicy: '查看完整协议',
|
||||||
|
agreeAndStart: '同意并开始'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
@@ -31,10 +40,12 @@ export default {
|
|||||||
goToBuildings: '前往建筑页面',
|
goToBuildings: '前往建筑页面',
|
||||||
locked: '已锁定',
|
locked: '已锁定',
|
||||||
viewRequirements: '查看前置条件',
|
viewRequirements: '查看前置条件',
|
||||||
|
requirements: '前置条件',
|
||||||
requirementsNotMet: '前置条件未满足',
|
requirementsNotMet: '前置条件未满足',
|
||||||
current: '当前',
|
current: '当前',
|
||||||
level: '等级',
|
level: '等级',
|
||||||
gmModeActivated: 'GM 模式已激活!请查看导航菜单。'
|
gmModeActivated: 'GM 模式已激活!请查看导航菜单。',
|
||||||
|
view: '查看'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: '不满足前置条件',
|
requirementsNotMet: '不满足前置条件',
|
||||||
@@ -112,7 +123,13 @@ export default {
|
|||||||
homePlanet: '母星',
|
homePlanet: '母星',
|
||||||
planetPrefix: '星球',
|
planetPrefix: '星球',
|
||||||
moonSuffix: '的月球',
|
moonSuffix: '的月球',
|
||||||
colonyPrefix: '殖民地'
|
colonyPrefix: '殖民地',
|
||||||
|
renamePlanet: '重命名星球',
|
||||||
|
renamePlanetTitle: '重命名星球',
|
||||||
|
newPlanetName: '新名称',
|
||||||
|
planetNamePlaceholder: '输入新的星球名称',
|
||||||
|
rename: '重命名',
|
||||||
|
renameSuccess: '星球已重命名为 {name}'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: '总积分'
|
points: '总积分'
|
||||||
@@ -126,6 +143,7 @@ export default {
|
|||||||
roboticsFactory: '机器人工厂',
|
roboticsFactory: '机器人工厂',
|
||||||
naniteFactory: '纳米工厂',
|
naniteFactory: '纳米工厂',
|
||||||
shipyard: '船坞',
|
shipyard: '船坞',
|
||||||
|
hangar: '机库',
|
||||||
researchLab: '研究实验室',
|
researchLab: '研究实验室',
|
||||||
metalStorage: '金属仓库',
|
metalStorage: '金属仓库',
|
||||||
crystalStorage: '晶体仓库',
|
crystalStorage: '晶体仓库',
|
||||||
@@ -164,6 +182,7 @@ export default {
|
|||||||
roboticsFactory: '加快建造速度',
|
roboticsFactory: '加快建造速度',
|
||||||
naniteFactory: '增加建造队列数量,每级+1队列(最多10级)',
|
naniteFactory: '增加建造队列数量,每级+1队列(最多10级)',
|
||||||
shipyard: '建造舰船',
|
shipyard: '建造舰船',
|
||||||
|
hangar: '专门用于扩展舰队存储容量,支持星球专业化发展',
|
||||||
researchLab: '研究科技',
|
researchLab: '研究科技',
|
||||||
metalStorage: '增加金属存储上限',
|
metalStorage: '增加金属存储上限',
|
||||||
crystalStorage: '增加晶体存储上限',
|
crystalStorage: '增加晶体存储上限',
|
||||||
@@ -198,10 +217,10 @@ export default {
|
|||||||
lightFighter: '基础战斗单位',
|
lightFighter: '基础战斗单位',
|
||||||
heavyFighter: '重装战斗机',
|
heavyFighter: '重装战斗机',
|
||||||
cruiser: '中型战舰,攻守平衡',
|
cruiser: '中型战舰,攻守平衡',
|
||||||
battleship: '强力战舰',
|
battleship: '主力重型战舰,拥有强大的火力和防护',
|
||||||
battlecruiser: '快速强大的战斗舰船,擅长攻击战列舰',
|
battlecruiser: '快速强大的战斗舰船,擅长攻击战列舰',
|
||||||
bomber: '专门对付防御设施的轰炸舰',
|
bomber: '专门对付防御设施的轰炸舰',
|
||||||
destroyer: '擅长摧毁大型舰船的猎杀者',
|
destroyer: '专业反大型舰船战舰,高火力低防护',
|
||||||
smallCargo: '运输少量资源',
|
smallCargo: '运输少量资源',
|
||||||
largeCargo: '运输大量资源',
|
largeCargo: '运输大量资源',
|
||||||
colonyShip: '用于殖民新星球',
|
colonyShip: '用于殖民新星球',
|
||||||
@@ -279,8 +298,9 @@ export default {
|
|||||||
ionTechnology: '离子武器技术',
|
ionTechnology: '离子武器技术',
|
||||||
hyperspaceTechnology: '超空间跳跃技术',
|
hyperspaceTechnology: '超空间跳跃技术',
|
||||||
plasmaTechnology: '等离子武器技术',
|
plasmaTechnology: '等离子武器技术',
|
||||||
computerTechnology: '增加研究队列数量,每级+1队列(最多10级)',
|
computerTechnology: '增加研究队列和舰队任务槽位,每级+1队列+1槽位(最多10级)',
|
||||||
espionageTechnology: '提高间谍探测效果,每级提高1级侦查深度',
|
espionageTechnology:
|
||||||
|
'提高间谍探测效果,每级提高1级侦查深度。侦察等级=己方等级-对方等级+侦察船数/5。≥-1显示舰队,≥1显示防御,≥3显示建筑,≥5显示科技',
|
||||||
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
weaponsTechnology: '提高舰船和防御的攻击力,每级+10%',
|
||||||
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
shieldingTechnology: '提高舰船和防御的护盾值,每级+10%',
|
||||||
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
armourTechnology: '提高舰船和防御的装甲值,每级+10%',
|
||||||
@@ -313,11 +333,14 @@ export default {
|
|||||||
darkMatterSpecialist: '提升暗物质采集效率'
|
darkMatterSpecialist: '提升暗物质采集效率'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: '进行中的任务',
|
||||||
|
empty: '当前没有进行中的队列',
|
||||||
buildQueueBonus: '建造队列',
|
buildQueueBonus: '建造队列',
|
||||||
spaceBonus: '空间加成',
|
spaceBonus: '空间加成',
|
||||||
researchQueueBonus: '研究队列',
|
researchQueueBonus: '研究队列',
|
||||||
building: '建造中',
|
building: '建造中',
|
||||||
researching: '研究中',
|
researching: '研究中',
|
||||||
|
demolishing: '拆除中',
|
||||||
remaining: '剩余时间',
|
remaining: '剩余时间',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
cancelBuild: '取消建造',
|
cancelBuild: '取消建造',
|
||||||
@@ -325,7 +348,14 @@ export default {
|
|||||||
confirmCancel: '确定要取消吗?将返还50%的资源。',
|
confirmCancel: '确定要取消吗?将返还50%的资源。',
|
||||||
level: '等级',
|
level: '等级',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
upgradeToLevel: '升级到等级'
|
upgradeToLevel: '升级到等级',
|
||||||
|
tabs: {
|
||||||
|
all: '全部',
|
||||||
|
buildings: '建筑',
|
||||||
|
research: '研究',
|
||||||
|
ships: '舰船',
|
||||||
|
defense: '防御'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: '星球总览',
|
title: '星球总览',
|
||||||
@@ -347,6 +377,7 @@ export default {
|
|||||||
level: '等级',
|
level: '等级',
|
||||||
upgradeCost: '升级消耗',
|
upgradeCost: '升级消耗',
|
||||||
buildTime: '建造时间',
|
buildTime: '建造时间',
|
||||||
|
build: '建造',
|
||||||
upgrade: '升级',
|
upgrade: '升级',
|
||||||
maxLevelReached: '等级已满',
|
maxLevelReached: '等级已满',
|
||||||
requirementsNotMet: '条件不足',
|
requirementsNotMet: '条件不足',
|
||||||
@@ -456,6 +487,7 @@ export default {
|
|||||||
colonize: '殖民',
|
colonize: '殖民',
|
||||||
spy: '侦察',
|
spy: '侦察',
|
||||||
deploy: '部署',
|
deploy: '部署',
|
||||||
|
expedition: '探险',
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
destroy: '行星毁灭',
|
destroy: '行星毁灭',
|
||||||
transportResources: '运输资源',
|
transportResources: '运输资源',
|
||||||
@@ -569,17 +601,20 @@ export default {
|
|||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
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}的星球'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: '消息中心',
|
title: '消息中心',
|
||||||
battles: '战斗',
|
battles: '战斗',
|
||||||
spy: '侦查',
|
spy: '侦查',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
battleReports: '战斗报告',
|
battleReports: '战斗报告',
|
||||||
spyReports: '间谍报告',
|
spyReports: '间谍报告',
|
||||||
noBattleReports: '暂无战斗报告',
|
noBattleReports: '暂无战斗报告',
|
||||||
noSpyReports: '暂无间谍报告',
|
noSpyReports: '暂无间谍报告',
|
||||||
|
noDiplomaticReports: '',
|
||||||
battleReport: '战斗报告',
|
battleReport: '战斗报告',
|
||||||
spyReport: '间谍报告',
|
spyReport: '间谍报告',
|
||||||
victory: '胜利',
|
victory: '胜利',
|
||||||
@@ -601,6 +636,7 @@ export default {
|
|||||||
targetPlanet: '目标星球',
|
targetPlanet: '目标星球',
|
||||||
attackerRemaining: '攻击方剩余',
|
attackerRemaining: '攻击方剩余',
|
||||||
defenderRemaining: '防守方剩余',
|
defenderRemaining: '防守方剩余',
|
||||||
|
allDestroyed: '全部摧毁',
|
||||||
moonChance: '月球生成概率',
|
moonChance: '月球生成概率',
|
||||||
showRoundDetails: '显示回合详情',
|
showRoundDetails: '显示回合详情',
|
||||||
hideRoundDetails: '隐藏回合详情',
|
hideRoundDetails: '隐藏回合详情',
|
||||||
@@ -635,7 +671,49 @@ export default {
|
|||||||
hostile: '对方对你有敌意,不接受礼物',
|
hostile: '对方对你有敌意,不接受礼物',
|
||||||
neutral_distrust: '对方对你缺乏信任',
|
neutral_distrust: '对方对你缺乏信任',
|
||||||
polite_decline: '对方礼貌地拒绝了'
|
polite_decline: '对方礼貌地拒绝了'
|
||||||
}
|
},
|
||||||
|
// 被侦查通知对话框
|
||||||
|
spiedNotificationDetails: '被侦查通知详情',
|
||||||
|
spyDetected: '侦查被发现',
|
||||||
|
detectionResult: '检测结果',
|
||||||
|
detectionSuccess: '你发现了敌方侦查!',
|
||||||
|
spiedNotificationMessage: '{npc}试图侦查你的星球{planet}',
|
||||||
|
spiedNotificationTip: '考虑增强防御或反击,如果这个NPC对你有敌意',
|
||||||
|
viewInGalaxy: '在星系中查看',
|
||||||
|
// 任务报告对话框
|
||||||
|
missionReportDetails: '任务报告详情',
|
||||||
|
missionSuccess: '成功',
|
||||||
|
missionFailed: '失败',
|
||||||
|
origin: '起点',
|
||||||
|
destination: '终点',
|
||||||
|
missionDetails: '任务详情',
|
||||||
|
transportedResources: '运输资源',
|
||||||
|
recycledResources: '回收资源',
|
||||||
|
remainingDebris: '剩余残骸',
|
||||||
|
newPlanet: '新星球',
|
||||||
|
// NPC活动对话框
|
||||||
|
npcActivityDetails: 'NPC活动详情',
|
||||||
|
activityType: {
|
||||||
|
recycle: '回收残骸'
|
||||||
|
},
|
||||||
|
activityLocation: '活动位置',
|
||||||
|
position: '位置',
|
||||||
|
nearPlanet: '附近星球',
|
||||||
|
activityDescription: '活动描述',
|
||||||
|
npcActivityMessage: '{npc}正在{position}{activity}',
|
||||||
|
arrivalTime: '到达时间',
|
||||||
|
npcActivityTip: 'NPC可能会收集战斗产生的残骸。如果你想竞争资源,可以尝试先到达该位置',
|
||||||
|
// 清空消息
|
||||||
|
clearMessages: '清空消息',
|
||||||
|
clearMessageTypes: '选择要清空的消息类型',
|
||||||
|
clearBattleReports: '战斗报告',
|
||||||
|
clearSpyReports: '间谍报告',
|
||||||
|
clearSpiedNotifications: '被侦查通知',
|
||||||
|
clearMissionReports: '任务报告',
|
||||||
|
clearNPCActivity: 'NPC活动',
|
||||||
|
clearGiftNotifications: '礼物通知',
|
||||||
|
clearGiftRejected: '拒绝记录',
|
||||||
|
clearNow: '立即清空'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: '运输任务成功完成',
|
transportSuccess: '运输任务成功完成',
|
||||||
@@ -651,7 +729,15 @@ export default {
|
|||||||
missileAttackSuccess: '导弹攻击成功',
|
missileAttackSuccess: '导弹攻击成功',
|
||||||
missileAttackFailed: '导弹攻击失败,目标星球不存在',
|
missileAttackFailed: '导弹攻击失败,目标星球不存在',
|
||||||
missileAttackIntercepted: '所有导弹被拦截',
|
missileAttackIntercepted: '所有导弹被拦截',
|
||||||
hits: '枚命中'
|
hits: '枚命中',
|
||||||
|
expeditionResources: '远征队发现了资源!',
|
||||||
|
expeditionDarkMatter: '远征队发现了暗物质!',
|
||||||
|
expeditionFleet: '远征队发现了废弃的舰船!',
|
||||||
|
expeditionPiratesAttack: '远征队遭遇海盗袭击,损失了部分舰船',
|
||||||
|
expeditionPiratesEscaped: '远征队遭遇海盗,但成功逃脱',
|
||||||
|
expeditionAliensAttack: '远征队遭遇外星人袭击,损失了部分舰船',
|
||||||
|
expeditionAliensEscaped: '远征队遭遇外星人,但成功逃脱',
|
||||||
|
expeditionNothing: '远征队什么也没有发现'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: '战斗模拟器',
|
title: '战斗模拟器',
|
||||||
@@ -741,7 +827,31 @@ export default {
|
|||||||
buildDate: '构建日期',
|
buildDate: '构建日期',
|
||||||
community: '社区',
|
community: '社区',
|
||||||
github: 'GitHub 仓库',
|
github: 'GitHub 仓库',
|
||||||
qqGroup: 'QQ 交流群'
|
qqGroup: 'QQ 交流群',
|
||||||
|
privacyPolicy: '隐私协议',
|
||||||
|
displaySettings: '显示设置',
|
||||||
|
displaySettingsDesc: '调整游戏的视觉效果',
|
||||||
|
backgroundAnimation: '背景动画',
|
||||||
|
backgroundAnimationDesc: '开启后显示星空/粒子背景动画(可能影响性能)',
|
||||||
|
notifications: '通知设置',
|
||||||
|
notificationsDesc: '管理游戏内的通知提醒',
|
||||||
|
notificationTypes: '通知类型',
|
||||||
|
browserNotifications: '浏览器通知',
|
||||||
|
inAppNotifications: '页面内通知',
|
||||||
|
constructionComplete: '建筑完成',
|
||||||
|
researchComplete: '研究完成',
|
||||||
|
browserPermission: '启用浏览器通知',
|
||||||
|
permissionGranted: '已获得权限',
|
||||||
|
permissionDenied: '权限被拒绝/未获得',
|
||||||
|
inAppNotificationsDesc: '通过页面弹窗显示',
|
||||||
|
notificationsDisabled: '启用上方任一开关以配置具体通知',
|
||||||
|
suppressInFocus: '页面聚焦时不发送浏览器通知',
|
||||||
|
expandTypes: '展开详细设置',
|
||||||
|
collapseTypes: '收起详细设置'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: '建造完成',
|
||||||
|
researchComplete: '研究完成'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM 管理面板',
|
title: 'GM 管理面板',
|
||||||
@@ -801,9 +911,11 @@ export default {
|
|||||||
completeAllQueues: '一键完成所有队列',
|
completeAllQueues: '一键完成所有队列',
|
||||||
completeAllQueuesDesc: '立即完成所有建筑、科技、舰船、防御队列和飞行任务',
|
completeAllQueuesDesc: '立即完成所有建筑、科技、舰船、防御队列和飞行任务',
|
||||||
completeQueues: '完成队列',
|
completeQueues: '完成队列',
|
||||||
completeQueuesSuccess: '已完成 {buildingCount} 个建筑队列、{researchCount} 个科技队列、{missionCount} 个飞行任务、{missileCount} 个导弹任务'
|
completeQueuesSuccess:
|
||||||
|
'已完成 {buildingCount} 个建筑队列、{researchCount} 个科技队列、{missionCount} 个飞行任务、{missileCount} 个导弹任务'
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count}支敌方舰队来袭',
|
||||||
npcSpyIncoming: 'NPC侦查即将到达',
|
npcSpyIncoming: 'NPC侦查即将到达',
|
||||||
npcAttackIncoming: 'NPC舰队来袭!',
|
npcAttackIncoming: 'NPC舰队来袭!',
|
||||||
npcFleetIncoming: 'NPC舰队接近',
|
npcFleetIncoming: 'NPC舰队接近',
|
||||||
@@ -815,6 +927,30 @@ export default {
|
|||||||
npcSpiedYourPlanet: 'NPC侦查了你的星球',
|
npcSpiedYourPlanet: 'NPC侦查了你的星球',
|
||||||
npcAttackedYourPlanet: 'NPC攻击了你的星球'
|
npcAttackedYourPlanet: 'NPC攻击了你的星球'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: '敌方警报',
|
||||||
|
markAllRead: '全部已读',
|
||||||
|
noAlerts: '暂无警报',
|
||||||
|
fleetSize: '舰队规模',
|
||||||
|
ships: '艘',
|
||||||
|
viewFleet: '查看舰队',
|
||||||
|
alertDetails: '警报详情',
|
||||||
|
targetInfo: '目标信息',
|
||||||
|
arrivalTime: '到达时间',
|
||||||
|
countdown: '倒计时',
|
||||||
|
viewMessages: '查看消息',
|
||||||
|
arrived: '已到达',
|
||||||
|
missionType: {
|
||||||
|
spy: '侦查',
|
||||||
|
attack: '攻击',
|
||||||
|
unknown: '未知'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: '敌方侦查即将到达!',
|
||||||
|
attack: '敌方攻击即将到达!',
|
||||||
|
unknown: '敌方舰队即将到达!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: '外交',
|
title: '外交',
|
||||||
description: '管理与NPC的外交关系',
|
description: '管理与NPC的外交关系',
|
||||||
@@ -831,6 +967,10 @@ export default {
|
|||||||
recentEvents: '最近事件',
|
recentEvents: '最近事件',
|
||||||
recentEventsDescription: '最近的外交活动记录',
|
recentEventsDescription: '最近的外交活动记录',
|
||||||
ago: '前',
|
ago: '前',
|
||||||
|
notifications: '外交通知',
|
||||||
|
markAllRead: '全部已读',
|
||||||
|
noReports: '暂无外交事件',
|
||||||
|
viewAll: '查看全部',
|
||||||
status: {
|
status: {
|
||||||
friendly: '友好',
|
friendly: '友好',
|
||||||
neutral: '中立',
|
neutral: '中立',
|
||||||
@@ -843,9 +983,21 @@ export default {
|
|||||||
more: '更多',
|
more: '更多',
|
||||||
actions: {
|
actions: {
|
||||||
gift: '赠送资源',
|
gift: '赠送资源',
|
||||||
viewPlanets: '查看星球'
|
viewPlanets: '查看星球',
|
||||||
|
addNote: '添加备注',
|
||||||
|
editNote: '编辑备注'
|
||||||
},
|
},
|
||||||
|
note: '备注',
|
||||||
|
notePlaceholder: '输入备注...',
|
||||||
|
noteEmpty: '无备注',
|
||||||
lastEvent: '最近活动',
|
lastEvent: '最近活动',
|
||||||
|
reportDetails: '外交报告详情',
|
||||||
|
eventDescription: '事件描述',
|
||||||
|
reputationChange: '好感度变化',
|
||||||
|
before: '之前',
|
||||||
|
after: '之后',
|
||||||
|
statusChange: '关系状态变化',
|
||||||
|
viewDiplomacy: '查看外交页面',
|
||||||
events: {
|
events: {
|
||||||
gift: '赠送资源',
|
gift: '赠送资源',
|
||||||
attack: '攻击',
|
attack: '攻击',
|
||||||
@@ -853,6 +1005,15 @@ export default {
|
|||||||
spy: '侦查',
|
spy: '侦查',
|
||||||
stealDebris: '抢夺残骸'
|
stealDebris: '抢夺残骸'
|
||||||
},
|
},
|
||||||
|
eventType: {
|
||||||
|
gift: '赠送了资源',
|
||||||
|
attack: '发起了攻击',
|
||||||
|
allyAttacked: '攻击了盟友',
|
||||||
|
spy: '进行了侦查',
|
||||||
|
stealDebris: '抢夺了残骸',
|
||||||
|
destroyPlanet: '摧毁了星球',
|
||||||
|
unknown: '未知事件'
|
||||||
|
},
|
||||||
reports: {
|
reports: {
|
||||||
giftedResources: '赠送了 {metal}金属 {crystal}晶体 {deuterium}氘',
|
giftedResources: '赠送了 {metal}金属 {crystal}晶体 {deuterium}氘',
|
||||||
receivedGiftFromPlayer: '收到玩家的礼物',
|
receivedGiftFromPlayer: '收到玩家的礼物',
|
||||||
@@ -873,7 +1034,57 @@ export default {
|
|||||||
receivedGiftFromNpc: '收到了{npcName}的礼物',
|
receivedGiftFromNpc: '收到了{npcName}的礼物',
|
||||||
acceptedGiftFromNpc: '你接受了{npcName}的礼物:{metal}金属 {crystal}晶体 {deuterium}氘',
|
acceptedGiftFromNpc: '你接受了{npcName}的礼物:{metal}金属 {crystal}晶体 {deuterium}氘',
|
||||||
playerRejectedGift: '玩家拒绝了礼物',
|
playerRejectedGift: '玩家拒绝了礼物',
|
||||||
rejectedGiftFromNpc: '你拒绝了{npcName}的礼物。好感度{reputation}'
|
rejectedGiftFromNpc: '你拒绝了{npcName}的礼物。好感度{reputation}',
|
||||||
|
destroyedNpcPlanet: '摧毁了{npcName}的{planetName}',
|
||||||
|
playerDestroyedPlanet: '玩家摧毁了{planetName}',
|
||||||
|
youDestroyedNpcPlanet: '你摧毁了{npcName}的{planetName}。好感度{reputation}',
|
||||||
|
playerDestroyedAllyPlanet: '玩家摧毁了盟友{allyName}的{planetName}',
|
||||||
|
allyOutraged: '{allyName}对你摧毁盟友{targetName}的{planetName}感到愤怒',
|
||||||
|
npcEliminated: 'NPC {npcName}已被彻底消灭',
|
||||||
|
npcEliminatedMessage: '你消灭了{npcName}的所有星球!该势力已被彻底摧毁。'
|
||||||
|
},
|
||||||
|
searchPlaceholder: '搜索NPC名称...',
|
||||||
|
viewMode: {
|
||||||
|
card: '卡片',
|
||||||
|
list: '列表'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC状态诊断',
|
||||||
|
title: 'NPC状态诊断',
|
||||||
|
description: '当前玩家积分:{points},侦查间隔:{spyInterval}分钟,攻击间隔:{attackInterval}分钟,攻击概率:{attackProb}%',
|
||||||
|
noData: '暂无NPC数据',
|
||||||
|
difficulty: '难度',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: '简单',
|
||||||
|
medium: '普通',
|
||||||
|
hard: '困难'
|
||||||
|
},
|
||||||
|
reputation: '好感度',
|
||||||
|
spyProbes: '侦察机数量',
|
||||||
|
fleetPower: '舰队战力',
|
||||||
|
canSpy: '可以侦查',
|
||||||
|
canAttack: '可以攻击',
|
||||||
|
attackProbability: '攻击概率',
|
||||||
|
nextSpy: '下次侦查',
|
||||||
|
nextAttack: '下次攻击',
|
||||||
|
yes: '是',
|
||||||
|
no: '否',
|
||||||
|
timeFormat: '{min}分{sec}秒',
|
||||||
|
anytime: '随时可能',
|
||||||
|
statusExplanation: '状态说明',
|
||||||
|
noRelation: '无关系',
|
||||||
|
noRelationNeutral: '无关系(中立)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: '关系友好,不会主动行动',
|
||||||
|
neutralNoAction: '关系中立,不会主动行动',
|
||||||
|
hostileWillAct: '关系敌对,可能采取行动',
|
||||||
|
noRelationNeutral: '无外交关系,视为中立',
|
||||||
|
insufficientProbes: '侦察机不足(当前:{current},需要:{required})',
|
||||||
|
noFleet: '没有战斗舰队',
|
||||||
|
spyCooldown: '侦查冷却中({min}分{sec}秒)',
|
||||||
|
attackCooldown: '攻击冷却中({min}分{sec}秒)',
|
||||||
|
notSpiedYet: '尚未侦查过,需要先进行侦查'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -887,5 +1098,251 @@ export default {
|
|||||||
title: '页面未找到',
|
title: '页面未找到',
|
||||||
description: '抱歉,您访问的页面不存在',
|
description: '抱歉,您访问的页面不存在',
|
||||||
goHome: '返回首页'
|
goHome: '返回首页'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: '隐私协议',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: '简介',
|
||||||
|
content: '本隐私协议说明了 OGame-Vue-Ts 如何处理您的数据。我们致力于保护您的隐私,本游戏的设计完全尊重用户隐私。'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: '数据收集',
|
||||||
|
content: '本游戏仅在您的本地浏览器中收集和存储以下数据:',
|
||||||
|
items: {
|
||||||
|
gameProgress: '游戏进度(建筑等级、舰队、资源等)',
|
||||||
|
settings: '游戏设置(通知偏好、显示选项等)',
|
||||||
|
language: '语言偏好'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: '数据存储',
|
||||||
|
content:
|
||||||
|
'所有数据均存储在您浏览器的本地存储(localStorage)中。这意味着您的数据始终保留在您自己的设备上,我们无法访问、查看或收集任何您的游戏数据。'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: '无服务器通信',
|
||||||
|
content:
|
||||||
|
'本游戏是一个完全离线的单机游戏。除了检查更新功能(从 GitHub 获取版本信息)外,游戏不会与任何服务器通信。您的游戏数据永远不会离开您的设备。'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: '第三方服务',
|
||||||
|
content:
|
||||||
|
'本游戏使用第三方流量分析服务来统计访问量和流量来源,帮助我们了解用户使用情况并改进游戏体验。这些分析数据是匿名的,不包含任何个人身份信息。我们不使用任何广告服务或其他商业追踪工具。'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: '数据控制',
|
||||||
|
content: '您对自己的数据拥有完全控制权:',
|
||||||
|
items: {
|
||||||
|
export: '您可以随时导出游戏数据',
|
||||||
|
import: '您可以从备份文件导入数据',
|
||||||
|
delete: '您可以通过清除浏览器数据或使用游戏内的"清除数据"功能来删除所有数据'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: '联系我们',
|
||||||
|
content: '如果您对本隐私协议有任何问题,请通过以下方式联系我们:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: '天',
|
||||||
|
hours: '小时',
|
||||||
|
minutes: '分钟',
|
||||||
|
seconds: '秒'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
progress: '进度',
|
||||||
|
previous: '上一步',
|
||||||
|
next: '下一步',
|
||||||
|
gotIt: '我知道了',
|
||||||
|
completeButton: '完成',
|
||||||
|
skip: '跳过引导',
|
||||||
|
welcome: {
|
||||||
|
title: '欢迎来到 OGame',
|
||||||
|
content: '欢迎,指挥官!本教程将引导您了解建立帝国的基础知识。点击"下一步"开始您的征程。'
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
title: '资源概览',
|
||||||
|
content: '这些是您的资源:金属、晶体和重氢。它们是建造建筑和研究科技的必需品。能量也很重要,用于为您的基础设施供电。'
|
||||||
|
},
|
||||||
|
planet: {
|
||||||
|
title: '您的星球',
|
||||||
|
content: '这是您的母星。您可以在这里查看星球名称、坐标,并在扩张帝国时切换星球。'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
title: '导航菜单',
|
||||||
|
content: '使用此菜单在不同部分之间导航:建筑、研究、舰队、星系等。每个部分都提供独特的游戏功能。'
|
||||||
|
},
|
||||||
|
gotoBuildings: {
|
||||||
|
title: '前往建筑页面',
|
||||||
|
content: '让我们从建造一些建筑开始。点击"建筑"菜单项查看可用建筑。'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: '建造太阳能电站',
|
||||||
|
content: '首先建造太阳能电站!它为您的星球提供能量。没有能量,其他资源建筑无法运作。这是最重要的第一步。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建造队列',
|
||||||
|
content:
|
||||||
|
'您的建筑现在在建造队列中。点击右上角的队列图标可以查看所有正在进行的建造和研究任务。建筑需要时间完成,但您可以在等待时继续操作。'
|
||||||
|
},
|
||||||
|
buildMetalMine: {
|
||||||
|
title: '建造金属矿',
|
||||||
|
content: '现在有了能量,可以建造金属矿了。金属矿是您的主要金属来源,金属几乎用于每个建筑和舰船。'
|
||||||
|
},
|
||||||
|
buildCrystalMine: {
|
||||||
|
title: '建造晶体矿',
|
||||||
|
content: '晶体更稀有但对高级科技至关重要。建造晶体矿开始收集这种宝贵的资源。'
|
||||||
|
},
|
||||||
|
buildDeuterium: {
|
||||||
|
title: '建造重氢合成器',
|
||||||
|
content: '重氢是舰船燃料和高级研究的必需品。建造重氢合成器开始生产这种关键资源。'
|
||||||
|
},
|
||||||
|
upgradeMines: {
|
||||||
|
title: '升级资源矿',
|
||||||
|
content: '接下来,您需要升级三种资源矿(金属、晶体、重氢)到2级,以满足建造机器人工厂的要求。资源充足后,继续升级它们。'
|
||||||
|
},
|
||||||
|
buildRobotics: {
|
||||||
|
title: '建造机器人工厂',
|
||||||
|
content: '机器人工厂可以大幅加快建造速度。它需要金属矿、晶体矿和重氢合成器各达到2级。建造它来提升建造效率!'
|
||||||
|
},
|
||||||
|
upgradeMinesForLab: {
|
||||||
|
title: '继续升级资源矿',
|
||||||
|
content: '现在需要将三种资源矿升级到3级,以满足研究实验室的建造要求。继续发展您的资源产能。'
|
||||||
|
},
|
||||||
|
buildResearchLab: {
|
||||||
|
title: '建造研究实验室',
|
||||||
|
content: '研究实验室是技术进步的基础。它需要三种资源矿各达到3级。建造它以解锁科技研究!'
|
||||||
|
},
|
||||||
|
gotoResearch: {
|
||||||
|
title: '前往研究页面',
|
||||||
|
content: '既然您有了研究实验室,点击"研究"菜单查看可用的科技。'
|
||||||
|
},
|
||||||
|
researchEnergy: {
|
||||||
|
title: '研究能量科技',
|
||||||
|
content: '能量科技可以提高您的能量产出并解锁高级建筑。这是最基础也是最重要的科技之一。'
|
||||||
|
},
|
||||||
|
shipyardIntro: {
|
||||||
|
title: '舰队与船坞',
|
||||||
|
content: '舰船让您能够探索星系、运输资源并保卫您的帝国。要建造舰船,您需要船坞(需要机器人工厂2级)。'
|
||||||
|
},
|
||||||
|
gotoBuildingsForShipyard: {
|
||||||
|
title: '返回建筑页面',
|
||||||
|
content: '返回建筑页面来建造您的船坞。'
|
||||||
|
},
|
||||||
|
buildShipyard: {
|
||||||
|
title: '建造船坞',
|
||||||
|
content: '船坞允许您建造舰船和防御系统。这对舰队行动至关重要。'
|
||||||
|
},
|
||||||
|
fleetIntro: {
|
||||||
|
title: '舰队行动',
|
||||||
|
content: '一旦您拥有舰船,就可以派遣它们执行任务:运输资源、殖民星球、攻击敌人或探索废墟场。'
|
||||||
|
},
|
||||||
|
galaxyIntro: {
|
||||||
|
title: '探索星系',
|
||||||
|
content: '星系视图显示其他星球、废墟场和扩张机会。使用它来侦察目标并规划您的战略。'
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
title: '教程完成!',
|
||||||
|
content:
|
||||||
|
'恭喜,指挥官!您现在了解了基础知识。继续建设您的帝国,研究科技,探索星系。记住:先发展能量,再建资源,然后是工厂和研究!祝您好运!'
|
||||||
|
},
|
||||||
|
// 移动端教程
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: '欢迎来到 OGame(移动版)',
|
||||||
|
content: '欢迎,指挥官!这是专为触摸屏设计的简化教程。我们将快速介绍核心功能,让您开始建设帝国。'
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
title: '顶部资源栏',
|
||||||
|
content: '顶部显示您的资源:金属、晶体和重氢。点击可查看详细生产信息。'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
title: '打开导航菜单',
|
||||||
|
content: '点击这个菜单图标打开导航栏,您可以访问建筑、研究、舰队等所有功能。'
|
||||||
|
},
|
||||||
|
gotoBuildings: {
|
||||||
|
title: '前往建筑页面',
|
||||||
|
content: '菜单已打开!现在点击"建筑"选项,开始建造基础设施。'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: '建造太阳能电站',
|
||||||
|
content: '首先建造太阳能电站!向下滚动找到它,点击卡片进行建造。能量是一切的基础。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建造队列',
|
||||||
|
content: '点击右上角的队列图标可以查看建造进度。您可以继续浏览其他页面,建造会在后台进行。'
|
||||||
|
},
|
||||||
|
buildMetalMine: {
|
||||||
|
title: '建造金属矿',
|
||||||
|
content: '有了能量后,建造金属矿。向下滚动找到金属矿,点击建造。'
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
title: '快速教程完成!',
|
||||||
|
content: '很好!您已经掌握了基础操作。继续建造晶体矿和重氢合成器,然后探索其他功能。记住:先能量,再资源!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: '关闭',
|
||||||
|
gotIt: '知道了',
|
||||||
|
dontShowAgain: '不再显示',
|
||||||
|
resetHints: '重置提示',
|
||||||
|
resetHintsDesc: '重新显示所有提示',
|
||||||
|
hintsEnabled: '启用提示',
|
||||||
|
hintsEnabledDesc: '访问页面时显示帮助提示',
|
||||||
|
overview: {
|
||||||
|
title: '星球总览',
|
||||||
|
message: '在这里查看星球资源、舰队状态和生产详情。经常查看以监控进度!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: '建筑',
|
||||||
|
message: '在这里建造和升级建筑。先建太阳能电站获取能量,然后是资源矿。提示:机器人工厂可加速建造!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: '研究实验室',
|
||||||
|
message: '研究科技以解锁新舰船、提升战斗力和推进文明。能量科技是个好起点!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: '船坞',
|
||||||
|
message: '建造舰船来探索、运输资源和保卫帝国。货船可以在星球之间运送资源。'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: '舰队指挥',
|
||||||
|
message: '派遣舰船执行任务:攻击敌人、运输资源、殖民新星球或探索废墟场。'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: '星系地图',
|
||||||
|
message: '探索星系寻找可殖民的空星球、可回收的废墟场和可攻击的敌人。先用间谍探测器侦查!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: '外交',
|
||||||
|
message: '管理与NPC的关系。送礼可提升声望,否则可能遭受敌对攻击。敌人的盟友也可能对你敌对!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: '消息',
|
||||||
|
message: '在这里查看战斗报告、间谍报告和外交通知。追踪你的活动和敌人遭遇。'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: '星球防御',
|
||||||
|
message: '建造防御设施保护星球免受攻击。护盾和炮塔可以威慑袭击者!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: '军官',
|
||||||
|
message: '招募军官获得各种加成!指挥官加速建造,地质学家提升资源产量,上将增强舰队能力。'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: '战斗模拟器',
|
||||||
|
message: '在发动攻击前模拟战斗结果。输入双方舰队和科技等级,预测胜负和损失。'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: '设置',
|
||||||
|
message: '在这里管理游戏数据、调整通知设置、导出/导入存档。记得定期备份你的进度!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM管理面板',
|
||||||
|
message: 'GM模式可以快速修改资源、建筑、科技等级。用于测试或体验完整游戏内容。'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
|
home: {
|
||||||
|
subtitle: '征服星辰大海',
|
||||||
|
startGame: '開始遊戲',
|
||||||
|
privacyAgreement: '隱私協議',
|
||||||
|
privacyAgreementDesc: '開始遊戲前,請閱讀並同意我們的隱私協議。',
|
||||||
|
agreeToPrivacy: '我已閱讀並同意',
|
||||||
|
viewFullPolicy: '查看完整協議',
|
||||||
|
agreeAndStart: '同意並開始'
|
||||||
|
},
|
||||||
common: {
|
common: {
|
||||||
confirm: '確認',
|
confirm: '確認',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
@@ -8,6 +17,7 @@ export default {
|
|||||||
close: '關閉',
|
close: '關閉',
|
||||||
back: '返回',
|
back: '返回',
|
||||||
next: '下一步',
|
next: '下一步',
|
||||||
|
gotIt: '',
|
||||||
previous: '上一步',
|
previous: '上一步',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
@@ -31,10 +41,12 @@ export default {
|
|||||||
goToBuildings: '前往建築頁面',
|
goToBuildings: '前往建築頁面',
|
||||||
locked: '已鎖定',
|
locked: '已鎖定',
|
||||||
viewRequirements: '查看前置條件',
|
viewRequirements: '查看前置條件',
|
||||||
|
requirements: '前置條件',
|
||||||
requirementsNotMet: '前置條件未滿足',
|
requirementsNotMet: '前置條件未滿足',
|
||||||
current: '當前',
|
current: '當前',
|
||||||
level: '等級',
|
level: '等級',
|
||||||
gmModeActivated: 'GM 模式已啟用!請查看導航選單。'
|
gmModeActivated: 'GM 模式已啟用!請查看導航選單。',
|
||||||
|
view: '查看'
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
requirementsNotMet: '不滿足前置條件',
|
requirementsNotMet: '不滿足前置條件',
|
||||||
@@ -56,7 +68,8 @@ export default {
|
|||||||
buildingLevelZero: '建築等級為0,無法拆除',
|
buildingLevelZero: '建築等級為0,無法拆除',
|
||||||
researchQueueFull: '研究隊列已滿',
|
researchQueueFull: '研究隊列已滿',
|
||||||
moonExists: '已存在月球',
|
moonExists: '已存在月球',
|
||||||
insufficientDebris: '殘骸場不足'
|
insufficientDebris: '殘骸場不足',
|
||||||
|
launchFailed: '發射失敗'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
overview: '總覽',
|
overview: '總覽',
|
||||||
@@ -111,7 +124,13 @@ export default {
|
|||||||
homePlanet: '母星',
|
homePlanet: '母星',
|
||||||
planetPrefix: '星球',
|
planetPrefix: '星球',
|
||||||
moonSuffix: '的月球',
|
moonSuffix: '的月球',
|
||||||
colonyPrefix: '殖民地'
|
colonyPrefix: '殖民地',
|
||||||
|
renamePlanet: '重命名星球',
|
||||||
|
renamePlanetTitle: '重命名星球',
|
||||||
|
newPlanetName: '新名稱',
|
||||||
|
planetNamePlaceholder: '輸入新的星球名稱',
|
||||||
|
rename: '重命名',
|
||||||
|
renameSuccess: '星球已重命名為 {name}'
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
points: '總積分'
|
points: '總積分'
|
||||||
@@ -125,6 +144,7 @@ export default {
|
|||||||
roboticsFactory: '機器人工廠',
|
roboticsFactory: '機器人工廠',
|
||||||
naniteFactory: '納米工廠',
|
naniteFactory: '納米工廠',
|
||||||
shipyard: '船塢',
|
shipyard: '船塢',
|
||||||
|
hangar: '機庫',
|
||||||
researchLab: '研究實驗室',
|
researchLab: '研究實驗室',
|
||||||
metalStorage: '金屬倉庫',
|
metalStorage: '金屬倉庫',
|
||||||
crystalStorage: '晶體倉庫',
|
crystalStorage: '晶體倉庫',
|
||||||
@@ -138,6 +158,7 @@ export default {
|
|||||||
jumpGate: '跳躍門',
|
jumpGate: '跳躍門',
|
||||||
planetDestroyerFactory: '行星毀滅者工廠',
|
planetDestroyerFactory: '行星毀滅者工廠',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
production: '產量',
|
production: '產量',
|
||||||
consumption: '消耗',
|
consumption: '消耗',
|
||||||
totalCost: '累積成本',
|
totalCost: '累積成本',
|
||||||
@@ -165,6 +186,7 @@ export default {
|
|||||||
roboticsFactory: '加快建造速度',
|
roboticsFactory: '加快建造速度',
|
||||||
naniteFactory: '增加建造佇列數量,每級+1佇列(最多10級)',
|
naniteFactory: '增加建造佇列數量,每級+1佇列(最多10級)',
|
||||||
shipyard: '建造艦船',
|
shipyard: '建造艦船',
|
||||||
|
hangar: '專門用於擴展艦隊儲存容量,支援行星專業化發展',
|
||||||
researchLab: '研究科技',
|
researchLab: '研究科技',
|
||||||
metalStorage: '增加金屬儲存上限',
|
metalStorage: '增加金屬儲存上限',
|
||||||
crystalStorage: '增加晶體儲存上限',
|
crystalStorage: '增加晶體儲存上限',
|
||||||
@@ -199,10 +221,10 @@ export default {
|
|||||||
lightFighter: '基礎戰鬥單位',
|
lightFighter: '基礎戰鬥單位',
|
||||||
heavyFighter: '重裝戰鬥機',
|
heavyFighter: '重裝戰鬥機',
|
||||||
cruiser: '中型戰艦,攻守平衡',
|
cruiser: '中型戰艦,攻守平衡',
|
||||||
battleship: '強力戰艦',
|
battleship: '主力重型戰艦,擁有強大的火力和防護',
|
||||||
battlecruiser: '快速強大的戰鬥艦船,擅長攻擊戰列艦',
|
battlecruiser: '快速強大的戰鬥艦船,擅長攻擊戰列艦',
|
||||||
bomber: '專門對付防禦設施的轟炸艦',
|
bomber: '專門對付防禦設施的轟炸艦',
|
||||||
destroyer: '擅長摧毀大型艦船的獵殺者',
|
destroyer: '專業反大型艦船戰艦,高火力低防護',
|
||||||
smallCargo: '運輸少量資源',
|
smallCargo: '運輸少量資源',
|
||||||
largeCargo: '運輸大量資源',
|
largeCargo: '運輸大量資源',
|
||||||
colonyShip: '用於殖民新星球',
|
colonyShip: '用於殖民新星球',
|
||||||
@@ -280,8 +302,9 @@ export default {
|
|||||||
ionTechnology: '離子武器技術',
|
ionTechnology: '離子武器技術',
|
||||||
hyperspaceTechnology: '超空間跳躍技術',
|
hyperspaceTechnology: '超空間跳躍技術',
|
||||||
plasmaTechnology: '等離子武器技術',
|
plasmaTechnology: '等離子武器技術',
|
||||||
computerTechnology: '增加研究佇列數量,每級+1佇列(最多10級)',
|
computerTechnology: '增加研究佇列和艦隊任務槽位,每級+1佇列+1槽位(最多10級)',
|
||||||
espionageTechnology: '提高間諜探測效果,每級提高1級偵查深度',
|
espionageTechnology:
|
||||||
|
'提高間諜探測效果,每級提高1級偵查深度。偵察等級=己方等級-對方等級+偵察船數/5。≥-1顯示艦隊,≥1顯示防禦,≥3顯示建築,≥5顯示科技',
|
||||||
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
weaponsTechnology: '提高艦船和防禦的攻擊力,每級+10%',
|
||||||
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
shieldingTechnology: '提高艦船和防禦的護盾值,每級+10%',
|
||||||
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
armourTechnology: '提高艦船和防禦的裝甲值,每級+10%',
|
||||||
@@ -314,10 +337,13 @@ export default {
|
|||||||
darkMatterSpecialist: '提升暗物質採集效率'
|
darkMatterSpecialist: '提升暗物質採集效率'
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
|
title: '進行中的任務',
|
||||||
|
empty: '當前沒有進行中的隊列',
|
||||||
buildQueue: '建造佇列',
|
buildQueue: '建造佇列',
|
||||||
researchQueue: '研究佇列',
|
researchQueue: '研究佇列',
|
||||||
building: '建造中',
|
building: '建造中',
|
||||||
researching: '研究中',
|
researching: '研究中',
|
||||||
|
demolishing: '拆除中',
|
||||||
remaining: '剩餘時間',
|
remaining: '剩餘時間',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
cancelBuild: '取消建造',
|
cancelBuild: '取消建造',
|
||||||
@@ -325,7 +351,14 @@ export default {
|
|||||||
confirmCancel: '確定要取消嗎?將返還50%的資源。',
|
confirmCancel: '確定要取消嗎?將返還50%的資源。',
|
||||||
level: '等級',
|
level: '等級',
|
||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeToLevel: '升級到等級'
|
upgradeToLevel: '升級到等級',
|
||||||
|
tabs: {
|
||||||
|
all: '全部',
|
||||||
|
buildings: '建築',
|
||||||
|
research: '研究',
|
||||||
|
ships: '艦船',
|
||||||
|
defense: '防禦'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
title: '星球總覽',
|
title: '星球總覽',
|
||||||
@@ -348,6 +381,7 @@ export default {
|
|||||||
gmModeActivated: '',
|
gmModeActivated: '',
|
||||||
upgradeCost: '升級消耗',
|
upgradeCost: '升級消耗',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '建造',
|
||||||
upgrade: '升級',
|
upgrade: '升級',
|
||||||
maxLevelReached: '等級已滿',
|
maxLevelReached: '等級已滿',
|
||||||
requirementsNotMet: '條件不足',
|
requirementsNotMet: '條件不足',
|
||||||
@@ -378,6 +412,7 @@ export default {
|
|||||||
fuelConsumption: '燃料消耗',
|
fuelConsumption: '燃料消耗',
|
||||||
buildCost: '建造成本',
|
buildCost: '建造成本',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
perUnit: '每個單位',
|
perUnit: '每個單位',
|
||||||
batchCalculator: '批量建造計算器',
|
batchCalculator: '批量建造計算器',
|
||||||
quantity: '數量',
|
quantity: '數量',
|
||||||
@@ -408,6 +443,7 @@ export default {
|
|||||||
armor: '裝甲',
|
armor: '裝甲',
|
||||||
buildCost: '建造成本',
|
buildCost: '建造成本',
|
||||||
buildTime: '建造時間',
|
buildTime: '建造時間',
|
||||||
|
build: '',
|
||||||
perUnit: '每個單位',
|
perUnit: '每個單位',
|
||||||
batchCalculator: '批量建造計算器',
|
batchCalculator: '批量建造計算器',
|
||||||
quantity: '數量',
|
quantity: '數量',
|
||||||
@@ -463,6 +499,7 @@ export default {
|
|||||||
colonize: '殖民',
|
colonize: '殖民',
|
||||||
spy: '偵察',
|
spy: '偵察',
|
||||||
deploy: '部署',
|
deploy: '部署',
|
||||||
|
expedition: '探險',
|
||||||
recycle: '回收',
|
recycle: '回收',
|
||||||
transportResources: '運輸資源',
|
transportResources: '運輸資源',
|
||||||
totalCargoCapacity: '總載貨量',
|
totalCargoCapacity: '總載貨量',
|
||||||
@@ -575,17 +612,20 @@ export default {
|
|||||||
sendGift: '贈送禮物',
|
sendGift: '贈送禮物',
|
||||||
debris: '殘骸',
|
debris: '殘骸',
|
||||||
giftPlanetTitle: '贈送禮物',
|
giftPlanetTitle: '贈送禮物',
|
||||||
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。'
|
giftPlanetMessage: '確定要向星球 [{coordinates}] 贈送資源嗎?\n\n請前往艦隊頁面選擇運輸船並裝載資源。',
|
||||||
|
npcPlanetName: '{name}的星球'
|
||||||
},
|
},
|
||||||
messagesView: {
|
messagesView: {
|
||||||
title: '訊息中心',
|
title: '訊息中心',
|
||||||
battles: '戰鬥',
|
battles: '戰鬥',
|
||||||
spy: '偵查',
|
spy: '偵查',
|
||||||
npc: 'NPC',
|
npc: 'NPC',
|
||||||
|
diplomacy: '',
|
||||||
battleReports: '戰鬥報告',
|
battleReports: '戰鬥報告',
|
||||||
spyReports: '間諜報告',
|
spyReports: '間諜報告',
|
||||||
noBattleReports: '暫無戰鬥報告',
|
noBattleReports: '暫無戰鬥報告',
|
||||||
noSpyReports: '暫無間諜報告',
|
noSpyReports: '暫無間諜報告',
|
||||||
|
noDiplomaticReports: '',
|
||||||
battleReport: '戰鬥報告',
|
battleReport: '戰鬥報告',
|
||||||
spyReport: '間諜報告',
|
spyReport: '間諜報告',
|
||||||
victory: '勝利',
|
victory: '勝利',
|
||||||
@@ -607,6 +647,7 @@ export default {
|
|||||||
targetPlanet: '目標星球',
|
targetPlanet: '目標星球',
|
||||||
attackerRemaining: '攻擊方剩餘',
|
attackerRemaining: '攻擊方剩餘',
|
||||||
defenderRemaining: '防守方剩餘',
|
defenderRemaining: '防守方剩餘',
|
||||||
|
allDestroyed: '全部摧毀',
|
||||||
moonChance: '月球生成機率',
|
moonChance: '月球生成機率',
|
||||||
showRoundDetails: '顯示回合詳情',
|
showRoundDetails: '顯示回合詳情',
|
||||||
hideRoundDetails: '隱藏回合詳情',
|
hideRoundDetails: '隱藏回合詳情',
|
||||||
@@ -641,7 +682,48 @@ export default {
|
|||||||
hostile: '對方對你有敵意,不接受禮物',
|
hostile: '對方對你有敵意,不接受禮物',
|
||||||
neutral_distrust: '對方對你缺乏信任',
|
neutral_distrust: '對方對你缺乏信任',
|
||||||
polite_decline: '對方禮貌地拒絕了'
|
polite_decline: '對方禮貌地拒絕了'
|
||||||
}
|
},
|
||||||
|
// Spied notification dialog
|
||||||
|
spiedNotificationDetails: '',
|
||||||
|
spyDetected: '',
|
||||||
|
detectionResult: '',
|
||||||
|
detectionSuccess: '',
|
||||||
|
spiedNotificationMessage: '',
|
||||||
|
spiedNotificationTip: '',
|
||||||
|
viewInGalaxy: '',
|
||||||
|
// Mission report dialog
|
||||||
|
missionReportDetails: '',
|
||||||
|
missionSuccess: '',
|
||||||
|
missionFailed: '',
|
||||||
|
origin: '',
|
||||||
|
destination: '',
|
||||||
|
missionDetails: '',
|
||||||
|
transportedResources: '',
|
||||||
|
recycledResources: '',
|
||||||
|
remainingDebris: '',
|
||||||
|
newPlanet: '',
|
||||||
|
// NPC activity dialog
|
||||||
|
npcActivityDetails: '',
|
||||||
|
activityType: {
|
||||||
|
recycle: ''
|
||||||
|
},
|
||||||
|
activityLocation: '',
|
||||||
|
position: '',
|
||||||
|
nearPlanet: '',
|
||||||
|
activityDescription: '',
|
||||||
|
npcActivityMessage: '',
|
||||||
|
arrivalTime: '',
|
||||||
|
npcActivityTip: '',
|
||||||
|
clearMessages: '清空訊息',
|
||||||
|
clearMessageTypes: '選擇要清空的訊息類型',
|
||||||
|
clearBattleReports: '戰鬥報告',
|
||||||
|
clearSpyReports: '間諜報告',
|
||||||
|
clearSpiedNotifications: '被偵查通知',
|
||||||
|
clearMissionReports: '任務報告',
|
||||||
|
clearNPCActivity: 'NPC活動',
|
||||||
|
clearGiftNotifications: '禮物通知',
|
||||||
|
clearGiftRejected: '拒絕記錄',
|
||||||
|
clearNow: '立即清空'
|
||||||
},
|
},
|
||||||
missionReports: {
|
missionReports: {
|
||||||
transportSuccess: '運輸任務成功完成',
|
transportSuccess: '運輸任務成功完成',
|
||||||
@@ -653,7 +735,19 @@ export default {
|
|||||||
recycleSuccess: '回收任務成功完成',
|
recycleSuccess: '回收任務成功完成',
|
||||||
recycleFailed: '回收任務失敗,目標位置沒有殘骸',
|
recycleFailed: '回收任務失敗,目標位置沒有殘骸',
|
||||||
destroySuccess: '行星毀滅任務成功執行',
|
destroySuccess: '行星毀滅任務成功執行',
|
||||||
destroyFailed: '行星毀滅任務失敗'
|
destroyFailed: '行星毀滅任務失敗',
|
||||||
|
expeditionResources: '探險隊發現了資源!',
|
||||||
|
expeditionDarkMatter: '探險隊發現了暗物質!',
|
||||||
|
expeditionFleet: '探險隊發現了廢棄的艦船!',
|
||||||
|
expeditionPiratesAttack: '探險隊遭遇海盜襲擊,損失了部分艦船',
|
||||||
|
expeditionPiratesEscaped: '探險隊遭遇海盜,但成功逃脫',
|
||||||
|
expeditionAliensAttack: '探險隊遭遇外星人襲擊,損失了部分艦船',
|
||||||
|
expeditionAliensEscaped: '探險隊遭遇外星人,但成功逃脫',
|
||||||
|
expeditionNothing: '探險隊什麼也沒有發現',
|
||||||
|
missileAttackSuccess: '導彈攻擊成功!',
|
||||||
|
missileAttackFailed: '導彈攻擊失敗',
|
||||||
|
missileAttackIntercepted: '導彈被攔截',
|
||||||
|
hits: '命中'
|
||||||
},
|
},
|
||||||
simulatorView: {
|
simulatorView: {
|
||||||
title: '戰鬥模擬器',
|
title: '戰鬥模擬器',
|
||||||
@@ -743,7 +837,27 @@ export default {
|
|||||||
buildDate: '建置日期',
|
buildDate: '建置日期',
|
||||||
community: '社群',
|
community: '社群',
|
||||||
github: 'GitHub 儲存庫',
|
github: 'GitHub 儲存庫',
|
||||||
qqGroup: 'QQ 交流群'
|
qqGroup: 'QQ 交流群',
|
||||||
|
privacyPolicy: '隱私協議',
|
||||||
|
notifications: '通知設定',
|
||||||
|
notificationsDesc: '管理遊戲內的通知提醒',
|
||||||
|
notificationTypes: '通知類型',
|
||||||
|
browserNotifications: '瀏覽器通知',
|
||||||
|
inAppNotifications: '頁面內通知',
|
||||||
|
constructionComplete: '建築完成',
|
||||||
|
researchComplete: '研究完成',
|
||||||
|
browserPermission: '啟用瀏覽器通知',
|
||||||
|
permissionGranted: '已獲得權限',
|
||||||
|
permissionDenied: '權限被拒絕/未獲得',
|
||||||
|
inAppNotificationsDesc: '透過頁面彈窗顯示',
|
||||||
|
notificationsDisabled: '啟用上方任一開關以配置具體通知',
|
||||||
|
suppressInFocus: '頁面聚焦時不發送瀏覽器通知',
|
||||||
|
expandTypes: '展開詳細設定',
|
||||||
|
collapseTypes: '收起詳細設定'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: '建造完成',
|
||||||
|
researchComplete: '研究完成'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM 管理面板',
|
title: 'GM 管理面板',
|
||||||
@@ -806,17 +920,42 @@ export default {
|
|||||||
completeQueuesSuccess: ''
|
completeQueuesSuccess: ''
|
||||||
},
|
},
|
||||||
alerts: {
|
alerts: {
|
||||||
|
incomingFleets: '{count}支敵方艦隊來襲',
|
||||||
npcSpyIncoming: 'NPC偵查即將到達',
|
npcSpyIncoming: 'NPC偵查即將到達',
|
||||||
npcAttackIncoming: 'NPC艦隊來襲!',
|
npcAttackIncoming: 'NPC艦隊來襲!',
|
||||||
npcFleetIncoming: 'NPC艦隊接近',
|
npcFleetIncoming: 'NPC艦隊接近',
|
||||||
ships: '艘艦船',
|
ships: '艘艦船',
|
||||||
spiedBy: '被偵查',
|
spiedBy: '被偵查',
|
||||||
attackedBy: '被攻擊',
|
attackedBy: '被攻擊',
|
||||||
detectionSuccess: '偵查被發現',
|
detectionSuccess: '你發現了敵方偵查!',
|
||||||
detectionFailed: '偵查未被發現',
|
detectionFailed: '偵查未被發現',
|
||||||
npcSpiedYourPlanet: 'NPC偵查了你的星球',
|
npcSpiedYourPlanet: 'NPC偵查了你的星球',
|
||||||
npcAttackedYourPlanet: 'NPC攻擊了你的星球'
|
npcAttackedYourPlanet: 'NPC攻擊了你的星球'
|
||||||
},
|
},
|
||||||
|
enemyAlert: {
|
||||||
|
title: '敵方警報',
|
||||||
|
markAllRead: '全部已讀',
|
||||||
|
noAlerts: '暫無警報',
|
||||||
|
fleetSize: '艦隊規模',
|
||||||
|
ships: '艘',
|
||||||
|
viewFleet: '查看艦隊',
|
||||||
|
alertDetails: '警報詳情',
|
||||||
|
targetInfo: '目標資訊',
|
||||||
|
arrivalTime: '到達時間',
|
||||||
|
countdown: '倒數計時',
|
||||||
|
viewMessages: '查看訊息',
|
||||||
|
arrived: '已到達',
|
||||||
|
missionType: {
|
||||||
|
spy: '偵查',
|
||||||
|
attack: '攻擊',
|
||||||
|
unknown: '未知'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
spy: '敵方偵查即將到達!',
|
||||||
|
attack: '敵方攻擊即將到達!',
|
||||||
|
unknown: '敵方艦隊即將到達!'
|
||||||
|
}
|
||||||
|
},
|
||||||
diplomacy: {
|
diplomacy: {
|
||||||
title: '外交',
|
title: '外交',
|
||||||
description: '管理與NPC的外交關係',
|
description: '管理與NPC的外交關係',
|
||||||
@@ -833,6 +972,10 @@ export default {
|
|||||||
recentEvents: '最近事件',
|
recentEvents: '最近事件',
|
||||||
recentEventsDescription: '最近的外交活動記錄',
|
recentEventsDescription: '最近的外交活動記錄',
|
||||||
ago: '前',
|
ago: '前',
|
||||||
|
notifications: '',
|
||||||
|
markAllRead: '',
|
||||||
|
noReports: '',
|
||||||
|
viewAll: '',
|
||||||
status: {
|
status: {
|
||||||
friendly: '友好',
|
friendly: '友好',
|
||||||
neutral: '中立',
|
neutral: '中立',
|
||||||
@@ -845,9 +988,30 @@ export default {
|
|||||||
more: '更多',
|
more: '更多',
|
||||||
actions: {
|
actions: {
|
||||||
gift: '贈送禮物',
|
gift: '贈送禮物',
|
||||||
viewPlanets: '查看星球'
|
viewPlanets: '查看星球',
|
||||||
|
addNote: '新增備註',
|
||||||
|
editNote: '編輯備註'
|
||||||
},
|
},
|
||||||
|
note: '備註',
|
||||||
|
notePlaceholder: '輸入備註...',
|
||||||
|
noteEmpty: '無備註',
|
||||||
lastEvent: '最近事件',
|
lastEvent: '最近事件',
|
||||||
|
reportDetails: '外交報告詳情',
|
||||||
|
eventDescription: '事件描述',
|
||||||
|
reputationChange: '好感度變化',
|
||||||
|
before: '之前',
|
||||||
|
after: '之後',
|
||||||
|
statusChange: '關係狀態變化',
|
||||||
|
viewDiplomacy: '查看外交頁面',
|
||||||
|
eventType: {
|
||||||
|
gift: '贈送了資源',
|
||||||
|
attack: '發起了攻擊',
|
||||||
|
allyAttacked: '攻擊了盟友',
|
||||||
|
spy: '進行了偵查',
|
||||||
|
stealDebris: '搶奪了殘骸',
|
||||||
|
destroyPlanet: '摧毀了星球',
|
||||||
|
unknown: '未知事件'
|
||||||
|
},
|
||||||
events: {
|
events: {
|
||||||
gift: '已贈送禮物',
|
gift: '已贈送禮物',
|
||||||
attack: '攻擊',
|
attack: '攻擊',
|
||||||
@@ -876,12 +1040,63 @@ export default {
|
|||||||
receivedGiftFromNpc: '收到了{npcName}的禮物',
|
receivedGiftFromNpc: '收到了{npcName}的禮物',
|
||||||
acceptedGiftFromNpc: '你接受了{npcName}的禮物:{metal}金屬 {crystal}晶體 {deuterium}氘',
|
acceptedGiftFromNpc: '你接受了{npcName}的禮物:{metal}金屬 {crystal}晶體 {deuterium}氘',
|
||||||
playerRejectedGift: '玩家拒絕了禮物',
|
playerRejectedGift: '玩家拒絕了禮物',
|
||||||
rejectedGiftFromNpc: '你拒絕了{npcName}的禮物。好感度{reputation}'
|
rejectedGiftFromNpc: '你拒絕了{npcName}的禮物。好感度{reputation}',
|
||||||
|
destroyedNpcPlanet: '摧毀了{npcName}的{planetName}',
|
||||||
|
playerDestroyedPlanet: '玩家摧毀了{planetName}',
|
||||||
|
youDestroyedNpcPlanet: '你摧毀了{npcName}的{planetName}。好感度{reputation}',
|
||||||
|
playerDestroyedAllyPlanet: '玩家摧毀了盟友{allyName}的{planetName}',
|
||||||
|
allyOutraged: '{allyName}對你摧毀盟友{targetName}的{planetName}感到憤怒',
|
||||||
|
npcEliminated: 'NPC {npcName}已被徹底消滅',
|
||||||
|
npcEliminatedMessage: '你消滅了{npcName}的所有星球!該勢力已被徹底摧毀。'
|
||||||
|
},
|
||||||
|
searchPlaceholder: '搜索NPC名稱...',
|
||||||
|
viewMode: {
|
||||||
|
card: '卡片',
|
||||||
|
list: '列表'
|
||||||
|
},
|
||||||
|
diagnostic: {
|
||||||
|
button: 'NPC狀態診斷',
|
||||||
|
title: 'NPC狀態診斷',
|
||||||
|
description: '當前玩家積分:{points},偵查間隔:{spyInterval}分鐘,攻擊間隔:{attackInterval}分鐘,攻擊概率:{attackProb}%',
|
||||||
|
noData: '暫無NPC數據',
|
||||||
|
difficulty: '難度',
|
||||||
|
difficultyLevels: {
|
||||||
|
easy: '簡單',
|
||||||
|
medium: '普通',
|
||||||
|
hard: '困難'
|
||||||
|
},
|
||||||
|
reputation: '好感度',
|
||||||
|
spyProbes: '偵察機數量',
|
||||||
|
fleetPower: '艦隊戰力',
|
||||||
|
canSpy: '可以偵查',
|
||||||
|
canAttack: '可以攻擊',
|
||||||
|
attackProbability: '攻擊概率',
|
||||||
|
nextSpy: '下次偵查',
|
||||||
|
nextAttack: '下次攻擊',
|
||||||
|
yes: '是',
|
||||||
|
no: '否',
|
||||||
|
timeFormat: '{min}分{sec}秒',
|
||||||
|
anytime: '隨時可能',
|
||||||
|
statusExplanation: '狀態說明',
|
||||||
|
noRelation: '無關係',
|
||||||
|
noRelationNeutral: '無關係(中立)',
|
||||||
|
reasons: {
|
||||||
|
friendlyNoAction: '關係友好,不會主動行動',
|
||||||
|
neutralNoAction: '關係中立,不會主動行動',
|
||||||
|
hostileWillAct: '關係敵對,可能採取行動',
|
||||||
|
noRelationNeutral: '無外交關係,視為中立',
|
||||||
|
insufficientProbes: '偵察機不足(當前:{current},需要:{required})',
|
||||||
|
noFleet: '沒有戰鬥艦隊',
|
||||||
|
spyCooldown: '偵查冷卻中({min}分{sec}秒)',
|
||||||
|
attackCooldown: '攻擊冷卻中({min}分{sec}秒)',
|
||||||
|
notSpiedYet: '尚未偵查過,需要先進行偵查'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: '上一頁',
|
previous: '上一頁',
|
||||||
next: '下一頁',
|
next: '下一頁',
|
||||||
|
gotIt: '',
|
||||||
first: '首頁',
|
first: '首頁',
|
||||||
last: '末頁',
|
last: '末頁',
|
||||||
page: '第 {page} 頁'
|
page: '第 {page} 頁'
|
||||||
@@ -890,5 +1105,143 @@ export default {
|
|||||||
title: '找不到頁面',
|
title: '找不到頁面',
|
||||||
description: '抱歉,您訪問的頁面不存在',
|
description: '抱歉,您訪問的頁面不存在',
|
||||||
goHome: '返回首頁'
|
goHome: '返回首頁'
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
title: '隱私協議',
|
||||||
|
sections: {
|
||||||
|
introduction: {
|
||||||
|
title: '簡介',
|
||||||
|
content: '本隱私協議說明了 OGame-Vue-Ts 如何處理您的資料。我們致力於保護您的隱私,本遊戲的設計完全尊重用戶隱私。'
|
||||||
|
},
|
||||||
|
dataCollection: {
|
||||||
|
title: '資料收集',
|
||||||
|
content: '本遊戲僅在您的本地瀏覽器中收集和存儲以下資料:',
|
||||||
|
items: {
|
||||||
|
gameProgress: '遊戲進度(建築等級、艦隊、資源等)',
|
||||||
|
settings: '遊戲設定(通知偏好、顯示選項等)',
|
||||||
|
language: '語言偏好'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataStorage: {
|
||||||
|
title: '資料存儲',
|
||||||
|
content:
|
||||||
|
'所有資料均存儲在您瀏覽器的本地存儲(localStorage)中。這意味著您的資料始終保留在您自己的設備上,我們無法訪問、查看或收集任何您的遊戲資料。'
|
||||||
|
},
|
||||||
|
noServer: {
|
||||||
|
title: '無伺服器通訊',
|
||||||
|
content:
|
||||||
|
'本遊戲是一個完全離線的單機遊戲。除了檢查更新功能(從 GitHub 獲取版本資訊)外,遊戲不會與任何伺服器通訊。您的遊戲資料永遠不會離開您的設備。'
|
||||||
|
},
|
||||||
|
thirdParty: {
|
||||||
|
title: '第三方服務',
|
||||||
|
content:
|
||||||
|
'本遊戲使用第三方流量分析服務來統計訪問量和流量來源,幫助我們了解用戶使用情況並改進遊戲體驗。這些分析資料是匿名的,不包含任何個人身份資訊。我們不使用任何廣告服務或其他商業追蹤工具。'
|
||||||
|
},
|
||||||
|
dataControl: {
|
||||||
|
title: '資料控制',
|
||||||
|
content: '您對自己的資料擁有完全控制權:',
|
||||||
|
items: {
|
||||||
|
export: '您可以隨時匯出遊戲資料',
|
||||||
|
import: '您可以從備份檔案匯入資料',
|
||||||
|
delete: '您可以通過清除瀏覽器資料或使用遊戲內的「清除資料」功能來刪除所有資料'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: '聯繫我們',
|
||||||
|
content: '如果您對本隱私協議有任何問題,請通過以下方式聯繫我們:'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
days: '天',
|
||||||
|
hours: '小時',
|
||||||
|
minutes: '分鐘',
|
||||||
|
seconds: '秒'
|
||||||
|
},
|
||||||
|
tutorial: {
|
||||||
|
welcome: {
|
||||||
|
title: '歡迎來到 OGame',
|
||||||
|
content: '歡迎,指揮官!讓我們從基礎開始,建立您的宇宙帝國。'
|
||||||
|
},
|
||||||
|
buildSolarPlant: {
|
||||||
|
title: '建造太陽能電站',
|
||||||
|
content: '首先建造太陽能電站!它為您的星球提供能量。沒有能量,其他資源建築無法運作。這是最重要的第一步。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建造佇列',
|
||||||
|
content:
|
||||||
|
'您的建築現在在建造佇列中。點擊右上角的佇列圖示可以查看所有正在進行的建造和研究任務。建築需要時間完成,但您可以在等待時繼續操作。'
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
welcome: {
|
||||||
|
title: '歡迎來到 OGame(移動版)',
|
||||||
|
content: '歡迎,指揮官!這是專為觸控螢幕設計的簡化教程。我們將快速介紹核心功能,讓您開始建設帝國。'
|
||||||
|
},
|
||||||
|
waitBuild: {
|
||||||
|
title: '建造佇列',
|
||||||
|
content: '點擊右上角的佇列圖示可以查看建造進度。您可以繼續瀏覽其他頁面,建造會在背景進行。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hints: {
|
||||||
|
close: '關閉',
|
||||||
|
gotIt: '知道了',
|
||||||
|
dontShowAgain: '不再顯示',
|
||||||
|
resetHints: '重置提示',
|
||||||
|
resetHintsDesc: '重新顯示所有提示',
|
||||||
|
hintsEnabled: '啟用提示',
|
||||||
|
hintsEnabledDesc: '訪問頁面時顯示幫助提示',
|
||||||
|
overview: {
|
||||||
|
title: '星球總覽',
|
||||||
|
message: '在這裡查看星球資源、艦隊狀態和生產詳情。經常查看以監控進度!'
|
||||||
|
},
|
||||||
|
buildings: {
|
||||||
|
title: '建築',
|
||||||
|
message: '在這裡建造和升級建築。先建太陽能電站獲取能量,然後是資源礦。提示:機器人工廠可加速建造!'
|
||||||
|
},
|
||||||
|
research: {
|
||||||
|
title: '研究實驗室',
|
||||||
|
message: '研究科技以解鎖新艦船、提升戰鬥力和推進文明。能量科技是個好起點!'
|
||||||
|
},
|
||||||
|
shipyard: {
|
||||||
|
title: '船塢',
|
||||||
|
message: '建造艦船來探索、運輸資源和保衛帝國。貨船可以在星球之間運送資源。'
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
title: '艦隊指揮',
|
||||||
|
message: '派遣艦船執行任務:攻擊敵人、運輸資源、殖民新星球或探索廢墟場。'
|
||||||
|
},
|
||||||
|
galaxy: {
|
||||||
|
title: '星系地圖',
|
||||||
|
message: '探索星系尋找可殖民的空星球、可回收的廢墟場和可攻擊的敵人。先用間諜探測器偵查!'
|
||||||
|
},
|
||||||
|
diplomacy: {
|
||||||
|
title: '外交',
|
||||||
|
message: '管理與NPC的關係。送禮可提升聲望,否則可能遭受敵對攻擊。敵人的盟友也可能對你敵對!'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
title: '訊息',
|
||||||
|
message: '在這裡查看戰鬥報告、間諜報告和外交通知。追蹤你的活動和敵人遭遇。'
|
||||||
|
},
|
||||||
|
defense: {
|
||||||
|
title: '星球防禦',
|
||||||
|
message: '建造防禦設施保護星球免受攻擊。護盾和砲塔可以威懾襲擊者!'
|
||||||
|
},
|
||||||
|
officers: {
|
||||||
|
title: '軍官',
|
||||||
|
message: '招募軍官獲得各種加成!指揮官加速建造,地質學家提升資源產量,上將增強艦隊能力。'
|
||||||
|
},
|
||||||
|
simulator: {
|
||||||
|
title: '戰鬥模擬器',
|
||||||
|
message: '在發動攻擊前模擬戰鬥結果。輸入雙方艦隊和科技等級,預測勝負和損失。'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: '設置',
|
||||||
|
message: '在這裡管理遊戲數據、調整通知設置、導出/導入存檔。記得定期備份你的進度!'
|
||||||
|
},
|
||||||
|
gm: {
|
||||||
|
title: 'GM管理面板',
|
||||||
|
message: 'GM模式可以快速修改資源、建築、科技等級。用於測試或體驗完整遊戲內容。'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,7 @@ export const simulateBattle = async (
|
|||||||
|
|
||||||
// 计算月球生成概率(根据残骸场总量)
|
// 计算月球生成概率(根据残骸场总量)
|
||||||
const totalDebris = debrisField.metal + debrisField.crystal
|
const totalDebris = debrisField.metal + debrisField.crystal
|
||||||
const moonChance = Math.min(
|
const moonChance = Math.min(MOON_CONFIG.baseChance + Math.floor(totalDebris / MOON_CONFIG.chancePerDebris), MOON_CONFIG.maxChance) / 100 // 转换为0-1的概率
|
||||||
(MOON_CONFIG.baseChance + Math.floor(totalDebris / MOON_CONFIG.chancePerDebris)),
|
|
||||||
MOON_CONFIG.maxChance
|
|
||||||
) / 100 // 转换为0-1的概率
|
|
||||||
|
|
||||||
// 生成战斗报告
|
// 生成战斗报告
|
||||||
const battleResult: BattleResult = {
|
const battleResult: BattleResult = {
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: nu
|
|||||||
export const completeBuildQueue = (
|
export const completeBuildQueue = (
|
||||||
planet: Planet,
|
planet: Planet,
|
||||||
now: number,
|
now: number,
|
||||||
onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void
|
onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void,
|
||||||
|
onCompleted?: (type: 'building' | 'ship' | 'defense' | 'demolish', itemType: string, level?: number, quantity?: number) => void
|
||||||
): void => {
|
): void => {
|
||||||
planet.buildQueue = planet.buildQueue.filter(item => {
|
planet.buildQueue = planet.buildQueue.filter(item => {
|
||||||
if (now >= item.endTime) {
|
if (now >= item.endTime) {
|
||||||
@@ -137,6 +138,10 @@ export const completeBuildQueue = (
|
|||||||
const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel)
|
const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel)
|
||||||
onPointsEarned(points, 'building', item.itemType, newLevel)
|
onPointsEarned(points, 'building', item.itemType, newLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onCompleted) {
|
||||||
|
onCompleted('building', item.itemType, newLevel)
|
||||||
|
}
|
||||||
} else if (item.type === 'ship') {
|
} else if (item.type === 'ship') {
|
||||||
const shipType = item.itemType as ShipType
|
const shipType = item.itemType as ShipType
|
||||||
const quantity = item.quantity || 0
|
const quantity = item.quantity || 0
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||||
import { locales, type Locale } from '@/locales'
|
import { locales, type Locale } from '@/locales'
|
||||||
|
import * as resourceLogic from './resourceLogic'
|
||||||
|
import * as officerLogic from './officerLogic'
|
||||||
import type {
|
import type {
|
||||||
DiplomaticRelation,
|
DiplomaticRelation,
|
||||||
RelationStatus,
|
RelationStatus,
|
||||||
@@ -14,6 +16,7 @@ import type {
|
|||||||
Resources,
|
Resources,
|
||||||
Player,
|
Player,
|
||||||
NPC,
|
NPC,
|
||||||
|
Planet,
|
||||||
FleetMission,
|
FleetMission,
|
||||||
BattleResult,
|
BattleResult,
|
||||||
Position,
|
Position,
|
||||||
@@ -231,24 +234,7 @@ export const handleGiftArrival = (
|
|||||||
// 计算好感度增加值
|
// 计算好感度增加值
|
||||||
const reputationGain = calculateGiftReputationGain(mission.cargo)
|
const reputationGain = calculateGiftReputationGain(mission.cargo)
|
||||||
|
|
||||||
// 更新玩家对NPC的关系
|
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||||
if (!player.diplomaticRelations) {
|
|
||||||
player.diplomaticRelations = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, targetNpc.id)
|
|
||||||
player.diplomaticRelations[targetNpc.id] = updateReputation(
|
|
||||||
relation,
|
|
||||||
reputationGain,
|
|
||||||
DET.GiftResources,
|
|
||||||
t('diplomacy.reports.giftedResources', locale, {
|
|
||||||
metal: mission.cargo.metal.toString(),
|
|
||||||
crystal: mission.cargo.crystal.toString(),
|
|
||||||
deuterium: mission.cargo.deuterium.toString()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// 也更新NPC对玩家的关系(双向好感度)
|
|
||||||
if (!targetNpc.relations) {
|
if (!targetNpc.relations) {
|
||||||
targetNpc.relations = {}
|
targetNpc.relations = {}
|
||||||
}
|
}
|
||||||
@@ -258,7 +244,11 @@ export const handleGiftArrival = (
|
|||||||
npcRelation,
|
npcRelation,
|
||||||
reputationGain,
|
reputationGain,
|
||||||
DET.GiftResources,
|
DET.GiftResources,
|
||||||
t('diplomacy.reports.receivedGiftFromPlayer', locale)
|
t('diplomacy.reports.giftedResources', locale, {
|
||||||
|
metal: mission.cargo.metal.toString(),
|
||||||
|
crystal: mission.cargo.crystal.toString(),
|
||||||
|
deuterium: mission.cargo.deuterium.toString()
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// 生成外交报告
|
// 生成外交报告
|
||||||
@@ -359,20 +349,7 @@ export const handleAttackReputation = (
|
|||||||
reputationLoss = REPUTATION_CHANGES.ATTACK_WIN
|
reputationLoss = REPUTATION_CHANGES.ATTACK_WIN
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新玩家对被攻击NPC的关系
|
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||||
if (!attacker.diplomaticRelations) {
|
|
||||||
attacker.diplomaticRelations = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = getOrCreateRelation(attacker.diplomaticRelations, attacker.id, defender.id)
|
|
||||||
attacker.diplomaticRelations[defender.id] = updateReputation(
|
|
||||||
relation,
|
|
||||||
reputationLoss,
|
|
||||||
DET.Attack,
|
|
||||||
t('diplomacy.reports.attackedNpc', locale, { npcName: defender.name })
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新被攻击NPC对玩家的关系
|
|
||||||
if (!defender.relations) {
|
if (!defender.relations) {
|
||||||
defender.relations = {}
|
defender.relations = {}
|
||||||
}
|
}
|
||||||
@@ -492,19 +469,7 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
|
|||||||
|
|
||||||
if (npcOwner) {
|
if (npcOwner) {
|
||||||
// 这是在NPC星球位置回收残骸,视为抢夺
|
// 这是在NPC星球位置回收残骸,视为抢夺
|
||||||
if (!player.diplomaticRelations) {
|
// 更新NPC对玩家的关系(统一使用 npc.relations)
|
||||||
player.diplomaticRelations = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const relation = getOrCreateRelation(player.diplomaticRelations, player.id, npcOwner.id)
|
|
||||||
player.diplomaticRelations[npcOwner.id] = updateReputation(
|
|
||||||
relation,
|
|
||||||
REPUTATION_CHANGES.STEAL_DEBRIS,
|
|
||||||
DET.StealDebris,
|
|
||||||
t('diplomacy.reports.stoleDebrisFromTerritory', locale, { npcName: npcOwner.name })
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新NPC对玩家的关系
|
|
||||||
if (!npcOwner.relations) {
|
if (!npcOwner.relations) {
|
||||||
npcOwner.relations = {}
|
npcOwner.relations = {}
|
||||||
}
|
}
|
||||||
@@ -514,7 +479,7 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
|
|||||||
npcRelation,
|
npcRelation,
|
||||||
REPUTATION_CHANGES.STEAL_DEBRIS,
|
REPUTATION_CHANGES.STEAL_DEBRIS,
|
||||||
DET.StealDebris,
|
DET.StealDebris,
|
||||||
t('diplomacy.reports.playerStoleDebris', locale)
|
t('diplomacy.reports.stoleDebrisFromTerritory', locale, { npcName: npcOwner.name })
|
||||||
)
|
)
|
||||||
|
|
||||||
// 生成外交报告
|
// 生成外交报告
|
||||||
@@ -528,30 +493,153 @@ export const handleDebrisRecycleReputation = (player: Player, debrisPosition: Po
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理星球摧毁事件的好感度变化
|
||||||
|
* 摧毁星球是最严重的行为,直接导致敌对关系
|
||||||
|
* @param attacker 攻击者(玩家)
|
||||||
|
* @param destroyedPlanet 被摧毁的星球
|
||||||
|
* @param planetOwner 星球所有者(NPC)
|
||||||
|
* @param allNpcs 所有NPC列表
|
||||||
|
* @param locale 语言代码
|
||||||
|
*/
|
||||||
|
export const handlePlanetDestructionReputation = (
|
||||||
|
attacker: Player,
|
||||||
|
destroyedPlanet: Planet,
|
||||||
|
planetOwner: NPC,
|
||||||
|
allNpcs: NPC[],
|
||||||
|
locale: Locale
|
||||||
|
): void => {
|
||||||
|
const { HOSTILE_THRESHOLD } = DIPLOMATIC_CONFIG
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// 更新星球所有者对玩家的关系 - 直接设为敌对(统一使用 npc.relations)
|
||||||
|
if (!planetOwner.relations) {
|
||||||
|
planetOwner.relations = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerRelation = getOrCreateRelation(planetOwner.relations, planetOwner.id, attacker.id)
|
||||||
|
const ownerEventDescription = t('diplomacy.reports.playerDestroyedPlanet', locale, {
|
||||||
|
planetName: destroyedPlanet.name
|
||||||
|
})
|
||||||
|
|
||||||
|
planetOwner.relations[attacker.id] = {
|
||||||
|
...ownerRelation,
|
||||||
|
reputation: HOSTILE_THRESHOLD, // 直接设为敌对阈值
|
||||||
|
status: RS.Hostile,
|
||||||
|
lastUpdated: now,
|
||||||
|
history: [
|
||||||
|
...(ownerRelation.history || []),
|
||||||
|
{
|
||||||
|
timestamp: now,
|
||||||
|
change: HOSTILE_THRESHOLD - ownerRelation.reputation,
|
||||||
|
reason: DET.DestroyPlanet,
|
||||||
|
details: ownerEventDescription
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成外交报告
|
||||||
|
generateDiplomaticReport(
|
||||||
|
attacker,
|
||||||
|
planetOwner,
|
||||||
|
DET.DestroyPlanet,
|
||||||
|
HOSTILE_THRESHOLD,
|
||||||
|
t('diplomacy.reports.youDestroyedNpcPlanet', locale, {
|
||||||
|
npcName: planetOwner.name,
|
||||||
|
planetName: destroyedPlanet.name,
|
||||||
|
reputation: HOSTILE_THRESHOLD
|
||||||
|
}),
|
||||||
|
'diplomacy.reports.youDestroyedNpcPlanet',
|
||||||
|
{ npcName: planetOwner.name, planetName: destroyedPlanet.name, reputation: HOSTILE_THRESHOLD }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 检查盟友关系网络 - 摧毁星球对盟友的影响更严重
|
||||||
|
if (planetOwner.allies && planetOwner.allies.length > 0) {
|
||||||
|
handleAllyPlanetDestroyedReputation(attacker, planetOwner, destroyedPlanet, allNpcs, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理盟友星球被摧毁的好感度变化
|
||||||
|
* @param attacker 攻击者(玩家)
|
||||||
|
* @param attackedNpc 星球被摧毁的NPC
|
||||||
|
* @param destroyedPlanet 被摧毁的星球
|
||||||
|
* @param allNpcs 所有NPC列表
|
||||||
|
* @param locale 语言代码
|
||||||
|
*/
|
||||||
|
export const handleAllyPlanetDestroyedReputation = (
|
||||||
|
attacker: Player,
|
||||||
|
attackedNpc: NPC,
|
||||||
|
destroyedPlanet: Planet,
|
||||||
|
allNpcs: NPC[],
|
||||||
|
locale: Locale
|
||||||
|
): void => {
|
||||||
|
const { REPUTATION_CHANGES } = DIPLOMATIC_CONFIG
|
||||||
|
|
||||||
|
// 找到所有盟友
|
||||||
|
const allies = allNpcs.filter(npc => attackedNpc.allies?.includes(npc.id))
|
||||||
|
|
||||||
|
allies.forEach(ally => {
|
||||||
|
// 更新盟友对玩家的关系 - 摧毁盟友星球的惩罚是攻击的两倍
|
||||||
|
if (!ally.relations) {
|
||||||
|
ally.relations = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allyRelation = getOrCreateRelation(ally.relations, ally.id, attacker.id)
|
||||||
|
const reputationLoss = REPUTATION_CHANGES.ALLY_ATTACKED * 2 // 双倍惩罚
|
||||||
|
ally.relations[attacker.id] = updateReputation(
|
||||||
|
allyRelation,
|
||||||
|
reputationLoss,
|
||||||
|
DET.DestroyPlanet,
|
||||||
|
t('diplomacy.reports.playerDestroyedAllyPlanet', locale, { allyName: attackedNpc.name, planetName: destroyedPlanet.name })
|
||||||
|
)
|
||||||
|
|
||||||
|
// 生成外交报告
|
||||||
|
generateDiplomaticReport(
|
||||||
|
attacker,
|
||||||
|
ally,
|
||||||
|
DET.DestroyPlanet,
|
||||||
|
reputationLoss,
|
||||||
|
t('diplomacy.reports.allyOutraged', locale, {
|
||||||
|
allyName: ally.name,
|
||||||
|
targetName: attackedNpc.name,
|
||||||
|
planetName: destroyedPlanet.name
|
||||||
|
}),
|
||||||
|
'diplomacy.reports.allyOutraged',
|
||||||
|
{
|
||||||
|
allyName: ally.name,
|
||||||
|
targetName: attackedNpc.name,
|
||||||
|
planetName: destroyedPlanet.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成外交报告
|
* 生成外交报告
|
||||||
* @param player 玩家
|
* @param player 玩家
|
||||||
* @param npc NPC
|
* @param npc NPC
|
||||||
* @param eventType 事件类型
|
* @param eventType 事件类型
|
||||||
* @param reputationChange 好感度变化值
|
* @param reputationChange 好感度变化值
|
||||||
* @param message 消息内容
|
* @param message 消息内容(已弃用,用于向后兼容)
|
||||||
|
* @param messageKey 翻译键(可选)
|
||||||
|
* @param messageParams 翻译参数(可选)
|
||||||
*/
|
*/
|
||||||
const generateDiplomaticReport = (
|
const generateDiplomaticReport = (
|
||||||
player: Player,
|
player: Player,
|
||||||
npc: NPC,
|
npc: NPC,
|
||||||
eventType: DiplomaticEventType,
|
eventType: DiplomaticEventType,
|
||||||
reputationChange: number,
|
reputationChange: number,
|
||||||
message: string
|
message: string,
|
||||||
|
messageKey?: string,
|
||||||
|
messageParams?: Record<string, string | number>
|
||||||
): void => {
|
): void => {
|
||||||
if (!player.diplomaticReports) {
|
if (!player.diplomaticReports) {
|
||||||
player.diplomaticReports = []
|
player.diplomaticReports = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!player.diplomaticRelations) {
|
// 使用 npc.relations 作为唯一数据源
|
||||||
player.diplomaticRelations = {}
|
const relation = npc.relations?.[player.id] || initializeDiplomaticRelation(npc.id, player.id)
|
||||||
}
|
|
||||||
|
|
||||||
const relation = player.diplomaticRelations[npc.id] || initializeDiplomaticRelation(player.id, npc.id)
|
|
||||||
const oldStatus = relation.status
|
const oldStatus = relation.status
|
||||||
const newReputation = Math.max(
|
const newReputation = Math.max(
|
||||||
DIPLOMATIC_CONFIG.MIN_REPUTATION,
|
DIPLOMATIC_CONFIG.MIN_REPUTATION,
|
||||||
@@ -570,6 +658,8 @@ const generateDiplomaticReport = (
|
|||||||
oldStatus,
|
oldStatus,
|
||||||
newStatus,
|
newStatus,
|
||||||
message,
|
message,
|
||||||
|
messageKey,
|
||||||
|
messageParams,
|
||||||
read: false
|
read: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,14 +717,14 @@ export const handleNPCGiftToPlayer = (npc: NPC, player: Player, giftResources: R
|
|||||||
* @param locale 语言代码
|
* @param locale 语言代码
|
||||||
*/
|
*/
|
||||||
export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification, locale: Locale): void => {
|
export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNotification, locale: Locale): void => {
|
||||||
// 将资源添加到玩家主星球
|
// 将资源添加到玩家主星球(使用安全添加函数防止溢出)
|
||||||
if (player.planets && player.planets.length > 0) {
|
if (player.planets && player.planets.length > 0) {
|
||||||
const mainPlanet = player.planets[0]
|
const mainPlanet = player.planets[0]
|
||||||
if (mainPlanet) {
|
if (mainPlanet) {
|
||||||
mainPlanet.resources.metal += giftNotification.resources.metal
|
// 计算军官加成
|
||||||
mainPlanet.resources.crystal += giftNotification.resources.crystal
|
const bonuses = officerLogic.calculateActiveBonuses(player.officers, Date.now())
|
||||||
mainPlanet.resources.deuterium += giftNotification.resources.deuterium
|
// 使用安全添加函数,超出容量的资源会丢失
|
||||||
mainPlanet.resources.darkMatter += giftNotification.resources.darkMatter
|
resourceLogic.addResourcesSafely(mainPlanet, giftNotification.resources, bonuses.storageCapacityBonus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,19 +741,6 @@ export const acceptNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
|
|||||||
t('diplomacy.reports.giftedResourcesToPlayer', locale)
|
t('diplomacy.reports.giftedResourcesToPlayer', locale)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 也更新玩家对NPC的关系(收到礼物会增加好感)
|
|
||||||
if (!player.diplomaticRelations) {
|
|
||||||
player.diplomaticRelations = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerRelation = getOrCreateRelation(player.diplomaticRelations, player.id, npc.id)
|
|
||||||
player.diplomaticRelations[npc.id] = updateReputation(
|
|
||||||
playerRelation,
|
|
||||||
giftNotification.expectedReputationGain,
|
|
||||||
DET.GiftResources,
|
|
||||||
t('diplomacy.reports.receivedGiftFromNpc', locale, { npcName: npc.name })
|
|
||||||
)
|
|
||||||
|
|
||||||
// 生成外交报告
|
// 生成外交报告
|
||||||
generateDiplomaticReport(
|
generateDiplomaticReport(
|
||||||
player,
|
player,
|
||||||
@@ -724,3 +801,78 @@ export const rejectNPCGift = (player: Player, npc: NPC, giftNotification: GiftNo
|
|||||||
player.giftNotifications = player.giftNotifications.filter(n => n.id !== giftNotification.id)
|
player.giftNotifications = player.giftNotifications.filter(n => n.id !== giftNotification.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理NPC被彻底消灭(所有星球被摧毁)
|
||||||
|
* @param eliminatedNpc 被消灭的NPC
|
||||||
|
* @param player 玩家
|
||||||
|
* @param allNpcs 所有NPC列表
|
||||||
|
* @param locale 语言代码
|
||||||
|
*/
|
||||||
|
export const handleNPCElimination = (eliminatedNpc: NPC, player: Player, allNpcs: NPC[], locale: Locale): void => {
|
||||||
|
const { HOSTILE_THRESHOLD } = DIPLOMATIC_CONFIG
|
||||||
|
|
||||||
|
// 1. 将NPC对玩家的关系设为最低(敌对状态)
|
||||||
|
if (!eliminatedNpc.relations) {
|
||||||
|
eliminatedNpc.relations = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const relation = getOrCreateRelation(eliminatedNpc.relations, eliminatedNpc.id, player.id)
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
eliminatedNpc.relations[player.id] = {
|
||||||
|
...relation,
|
||||||
|
reputation: HOSTILE_THRESHOLD, // 设为敌对阈值
|
||||||
|
status: RS.Hostile,
|
||||||
|
lastUpdated: now,
|
||||||
|
history: [
|
||||||
|
...(relation.history || []),
|
||||||
|
{
|
||||||
|
timestamp: now,
|
||||||
|
change: HOSTILE_THRESHOLD - relation.reputation,
|
||||||
|
reason: DET.DestroyPlanet,
|
||||||
|
details: t('diplomacy.reports.npcEliminated', locale, { npcName: eliminatedNpc.name })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成外交报告
|
||||||
|
generateDiplomaticReport(
|
||||||
|
player,
|
||||||
|
eliminatedNpc,
|
||||||
|
DET.DestroyPlanet,
|
||||||
|
HOSTILE_THRESHOLD,
|
||||||
|
t('diplomacy.reports.npcEliminatedMessage', locale, { npcName: eliminatedNpc.name }),
|
||||||
|
'diplomacy.reports.npcEliminatedMessage',
|
||||||
|
{ npcName: eliminatedNpc.name }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. 从所有其他NPC的盟友列表中移除被消灭的NPC
|
||||||
|
allNpcs.forEach(npc => {
|
||||||
|
if (npc.id !== eliminatedNpc.id && npc.allies && npc.allies.includes(eliminatedNpc.id)) {
|
||||||
|
npc.allies = npc.allies.filter(allyId => allyId !== eliminatedNpc.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并处理被消灭的NPC(所有星球都被摧毁的NPC)
|
||||||
|
* @param allNpcs 所有NPC列表
|
||||||
|
* @param player 玩家
|
||||||
|
* @param locale 语言代码
|
||||||
|
* @returns 被消灭的NPC ID列表
|
||||||
|
*/
|
||||||
|
export const checkAndHandleEliminatedNPCs = (allNpcs: NPC[], player: Player, locale: Locale): string[] => {
|
||||||
|
const eliminatedNpcIds: string[] = []
|
||||||
|
|
||||||
|
allNpcs.forEach(npc => {
|
||||||
|
// 检查NPC是否还有星球
|
||||||
|
if (!npc.planets || npc.planets.length === 0) {
|
||||||
|
// NPC被彻底消灭
|
||||||
|
handleNPCElimination(npc, player, allNpcs, locale)
|
||||||
|
eliminatedNpcIds.push(npc.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return eliminatedNpcIds
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import * as battleLogic from './battleLogic'
|
|||||||
import * as moonLogic from './moonLogic'
|
import * as moonLogic from './moonLogic'
|
||||||
import * as moonValidation from './moonValidation'
|
import * as moonValidation from './moonValidation'
|
||||||
import * as diplomaticLogic from './diplomaticLogic'
|
import * as diplomaticLogic from './diplomaticLogic'
|
||||||
|
import * as resourceLogic from './resourceLogic'
|
||||||
|
import * as fleetStorageLogic from './fleetStorageLogic'
|
||||||
|
import * as officerLogic from './officerLogic'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算两个星球之间的距离
|
* 计算两个星球之间的距离
|
||||||
@@ -36,9 +39,10 @@ export const calculateDistance = (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算飞行时间
|
* 计算飞行时间
|
||||||
|
* 平衡后的时间倍率,确保游戏节奏合理
|
||||||
*/
|
*/
|
||||||
export const calculateFlightTime = (distance: number, minSpeed: number): number => {
|
export const calculateFlightTime = (distance: number, minSpeed: number): number => {
|
||||||
return Math.max(10, Math.floor((distance * 10000) / minSpeed)) // 至少10秒
|
return Math.max(10, Math.floor((distance * 50) / minSpeed)) // 至少10秒
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,8 +81,9 @@ export const processTransportArrival = (
|
|||||||
targetPlanet: Planet | undefined,
|
targetPlanet: Planet | undefined,
|
||||||
player?: Player,
|
player?: Player,
|
||||||
allNpcs?: NPC[],
|
allNpcs?: NPC[],
|
||||||
locale: Locale = 'zh-CN'
|
locale: Locale = 'zh-CN',
|
||||||
): { success: boolean; reputationGain?: number } => {
|
storageCapacityBonus: number = 0
|
||||||
|
): { success: boolean; reputationGain?: number; overflow?: Resources } => {
|
||||||
// 检查是否是赠送任务
|
// 检查是否是赠送任务
|
||||||
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)
|
||||||
@@ -100,13 +105,15 @@ export const processTransportArrival = (
|
|||||||
|
|
||||||
// 正常运输任务
|
// 正常运输任务
|
||||||
if (targetPlanet) {
|
if (targetPlanet) {
|
||||||
targetPlanet.resources.metal += mission.cargo.metal
|
// 使用安全添加函数,防止资源溢出
|
||||||
targetPlanet.resources.crystal += mission.cargo.crystal
|
const result = resourceLogic.addResourcesSafely(targetPlanet, mission.cargo, storageCapacityBonus)
|
||||||
targetPlanet.resources.deuterium += mission.cargo.deuterium
|
|
||||||
targetPlanet.resources.darkMatter += mission.cargo.darkMatter
|
|
||||||
mission.status = 'returning'
|
mission.status = 'returning'
|
||||||
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
|
||||||
return { success: true }
|
// 如果有溢出的资源,保留在cargo中返回给发送者
|
||||||
|
if (result.overflow.metal > 0 || result.overflow.crystal > 0 || result.overflow.deuterium > 0 || result.overflow.darkMatter > 0) {
|
||||||
|
mission.cargo = result.overflow
|
||||||
|
return { success: true, overflow: result.overflow }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mission.status = 'returning'
|
mission.status = 'returning'
|
||||||
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
mission.cargo = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||||
@@ -177,10 +184,10 @@ export const processAttackArrival = async (
|
|||||||
})
|
})
|
||||||
targetPlanet.defense = battleLogic.repairDefense(defenseBeforeBattle, targetPlanet.defense) as Record<DefenseType, number>
|
targetPlanet.defense = battleLogic.repairDefense(defenseBeforeBattle, targetPlanet.defense) as Record<DefenseType, number>
|
||||||
|
|
||||||
// 扣除掠夺的资源
|
// 扣除掠夺的资源(防止下溢到负数)
|
||||||
targetPlanet.resources.metal -= battleResult.plunder.metal
|
targetPlanet.resources.metal = Math.max(0, targetPlanet.resources.metal - battleResult.plunder.metal)
|
||||||
targetPlanet.resources.crystal -= battleResult.plunder.crystal
|
targetPlanet.resources.crystal = Math.max(0, targetPlanet.resources.crystal - battleResult.plunder.crystal)
|
||||||
targetPlanet.resources.deuterium -= battleResult.plunder.deuterium
|
targetPlanet.resources.deuterium = Math.max(0, targetPlanet.resources.deuterium - battleResult.plunder.deuterium)
|
||||||
|
|
||||||
mission.status = 'returning'
|
mission.status = 'returning'
|
||||||
|
|
||||||
@@ -310,6 +317,23 @@ export const processNPCAttackArrival = async (
|
|||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NPC攻击玩家后降低好感度
|
||||||
|
if (!npc.relations) {
|
||||||
|
npc.relations = {}
|
||||||
|
}
|
||||||
|
if (!npc.relations[defender.id]) {
|
||||||
|
npc.relations[defender.id] = diplomaticLogic.initializeDiplomaticRelation(npc.id, defender.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据战斗结果降低好感度
|
||||||
|
// NPC获胜降低更多好感度,失败降低较少
|
||||||
|
const reputationChange = battleResult.winner === 'attacker' ? -15 : -10
|
||||||
|
const relation = npc.relations[defender.id]
|
||||||
|
if (relation) {
|
||||||
|
diplomaticLogic.updateReputation(relation, reputationChange, 'attack', `NPC ${npc.name} attacked player ${defender.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
return { battleResult, moon, debrisField }
|
return { battleResult, moon, debrisField }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,18 +542,28 @@ export const processSpyArrival = (
|
|||||||
/**
|
/**
|
||||||
* 处理部署任务到达
|
* 处理部署任务到达
|
||||||
*/
|
*/
|
||||||
export const processDeployArrival = (mission: FleetMission, targetPlanet: Planet | undefined, playerId: string): boolean => {
|
export const processDeployArrival = (
|
||||||
|
mission: FleetMission,
|
||||||
|
targetPlanet: Planet | undefined,
|
||||||
|
playerId: string,
|
||||||
|
technologies: Record<TechnologyType, number>
|
||||||
|
): { success: boolean; overflow?: Partial<Record<ShipType, number>> } => {
|
||||||
if (!targetPlanet || targetPlanet.ownerId !== playerId) {
|
if (!targetPlanet || targetPlanet.ownerId !== playerId) {
|
||||||
mission.status = 'returning'
|
mission.status = 'returning'
|
||||||
return false
|
return { success: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
// 使用安全添加函数,防止舰队仓储溢出
|
||||||
targetPlanet.fleet[shipType as ShipType] += count
|
const result = fleetStorageLogic.addFleetSafely(targetPlanet, mission.fleet, technologies)
|
||||||
|
// 如果有溢出的舰船,保留在mission.fleet中返回给发送者
|
||||||
|
const hasOverflow = Object.keys(result.overflow).length > 0
|
||||||
|
if (hasOverflow) {
|
||||||
|
mission.fleet = result.overflow as Fleet
|
||||||
|
mission.status = 'returning'
|
||||||
|
return { success: true, overflow: result.overflow }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 部署任务直接完成,不返回
|
// 部署任务直接完成,不返回
|
||||||
return true
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -605,6 +639,162 @@ export const processRecycleArrival = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远征事件类型
|
||||||
|
*/
|
||||||
|
export type ExpeditionEventType = 'resources' | 'darkMatter' | 'fleet' | 'nothing' | 'pirates' | 'aliens'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远征结果
|
||||||
|
*/
|
||||||
|
export interface ExpeditionResult {
|
||||||
|
eventType: ExpeditionEventType
|
||||||
|
resources?: Partial<Resources>
|
||||||
|
fleet?: Partial<Fleet>
|
||||||
|
fleetLost?: Partial<Fleet>
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理远征任务到达
|
||||||
|
* 远征任务会随机触发各种事件
|
||||||
|
*/
|
||||||
|
export const processExpeditionArrival = (mission: FleetMission): ExpeditionResult => {
|
||||||
|
// 计算舰队总货舱容量
|
||||||
|
let totalCargoCapacity = 0
|
||||||
|
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||||
|
if (count > 0) {
|
||||||
|
const shipConfig = getShipCargoCapacity(shipType as ShipType)
|
||||||
|
totalCargoCapacity += shipConfig * count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机事件概率
|
||||||
|
const random = Math.random() * 100
|
||||||
|
let result: ExpeditionResult
|
||||||
|
|
||||||
|
if (random < 30) {
|
||||||
|
// 30% 概率发现资源
|
||||||
|
const resourceMultiplier = 0.1 + Math.random() * 0.3 // 10%-40% 的货舱容量
|
||||||
|
const resourceAmount = Math.floor(totalCargoCapacity * resourceMultiplier)
|
||||||
|
const metalAmount = Math.floor(resourceAmount * 0.5)
|
||||||
|
const crystalAmount = Math.floor(resourceAmount * 0.35)
|
||||||
|
const deuteriumAmount = Math.floor(resourceAmount * 0.15)
|
||||||
|
|
||||||
|
mission.cargo.metal += metalAmount
|
||||||
|
mission.cargo.crystal += crystalAmount
|
||||||
|
mission.cargo.deuterium += deuteriumAmount
|
||||||
|
|
||||||
|
result = {
|
||||||
|
eventType: 'resources',
|
||||||
|
resources: { metal: metalAmount, crystal: crystalAmount, deuterium: deuteriumAmount, darkMatter: 0, energy: 0 },
|
||||||
|
message: 'expedition.foundResources'
|
||||||
|
}
|
||||||
|
} else if (random < 40) {
|
||||||
|
// 10% 概率发现暗物质
|
||||||
|
const darkMatterAmount = Math.floor(50 + Math.random() * 150) // 50-200 暗物质
|
||||||
|
mission.cargo.darkMatter += darkMatterAmount
|
||||||
|
|
||||||
|
result = {
|
||||||
|
eventType: 'darkMatter',
|
||||||
|
resources: { metal: 0, crystal: 0, deuterium: 0, darkMatter: darkMatterAmount, energy: 0 },
|
||||||
|
message: 'expedition.foundDarkMatter'
|
||||||
|
}
|
||||||
|
} else if (random < 55) {
|
||||||
|
// 15% 概率发现废弃舰船
|
||||||
|
const foundFleet: Partial<Fleet> = {}
|
||||||
|
const possibleShips: ShipType[] = [ShipType.LightFighter, ShipType.HeavyFighter, ShipType.SmallCargo, ShipType.LargeCargo]
|
||||||
|
const shipTypeIndex = Math.floor(Math.random() * possibleShips.length)
|
||||||
|
const shipType = possibleShips[shipTypeIndex] ?? ShipType.LightFighter
|
||||||
|
const count = Math.floor(1 + Math.random() * 5) // 1-5 艘
|
||||||
|
foundFleet[shipType] = count
|
||||||
|
|
||||||
|
// 将发现的舰船添加到任务舰队中
|
||||||
|
mission.fleet[shipType] = (mission.fleet[shipType] || 0) + count
|
||||||
|
|
||||||
|
result = {
|
||||||
|
eventType: 'fleet',
|
||||||
|
fleet: foundFleet,
|
||||||
|
message: 'expedition.foundFleet'
|
||||||
|
}
|
||||||
|
} else if (random < 70) {
|
||||||
|
// 15% 概率遭遇海盗(损失部分舰队)
|
||||||
|
const fleetLost: Partial<Fleet> = {}
|
||||||
|
let hasLoss = false
|
||||||
|
|
||||||
|
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||||
|
if (count > 0 && Math.random() < 0.3) {
|
||||||
|
// 30% 概率损失该类型舰船
|
||||||
|
const lossCount = Math.max(1, Math.floor(count * 0.1)) // 损失10%,最少1艘
|
||||||
|
const actualLoss = Math.min(lossCount, count)
|
||||||
|
fleetLost[shipType as ShipType] = actualLoss
|
||||||
|
mission.fleet[shipType as ShipType] = count - actualLoss
|
||||||
|
hasLoss = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
eventType: 'pirates',
|
||||||
|
fleetLost: hasLoss ? fleetLost : undefined,
|
||||||
|
message: hasLoss ? 'expedition.piratesAttack' : 'expedition.piratesEscaped'
|
||||||
|
}
|
||||||
|
} else if (random < 80) {
|
||||||
|
// 10% 概率遭遇外星人(损失更多舰队)
|
||||||
|
const fleetLost: Partial<Fleet> = {}
|
||||||
|
let hasLoss = false
|
||||||
|
|
||||||
|
for (const [shipType, count] of Object.entries(mission.fleet)) {
|
||||||
|
if (count > 0 && Math.random() < 0.5) {
|
||||||
|
// 50% 概率损失该类型舰船
|
||||||
|
const lossCount = Math.max(1, Math.floor(count * 0.2)) // 损失20%,最少1艘
|
||||||
|
const actualLoss = Math.min(lossCount, count)
|
||||||
|
fleetLost[shipType as ShipType] = actualLoss
|
||||||
|
mission.fleet[shipType as ShipType] = count - actualLoss
|
||||||
|
hasLoss = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
eventType: 'aliens',
|
||||||
|
fleetLost: hasLoss ? fleetLost : undefined,
|
||||||
|
message: hasLoss ? 'expedition.aliensAttack' : 'expedition.aliensEscaped'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 20% 概率什么都没发现
|
||||||
|
result = {
|
||||||
|
eventType: 'nothing',
|
||||||
|
message: 'expedition.nothing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mission.status = 'returning'
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取舰船货舱容量
|
||||||
|
*/
|
||||||
|
const getShipCargoCapacity = (shipType: ShipType): number => {
|
||||||
|
const cargoCapacities: Record<ShipType, number> = {
|
||||||
|
[ShipType.LightFighter]: 50,
|
||||||
|
[ShipType.HeavyFighter]: 100,
|
||||||
|
[ShipType.Cruiser]: 800,
|
||||||
|
[ShipType.Battleship]: 1500,
|
||||||
|
[ShipType.Battlecruiser]: 750,
|
||||||
|
[ShipType.Bomber]: 500,
|
||||||
|
[ShipType.Destroyer]: 2000,
|
||||||
|
[ShipType.SmallCargo]: 5000,
|
||||||
|
[ShipType.LargeCargo]: 25000,
|
||||||
|
[ShipType.ColonyShip]: 7500,
|
||||||
|
[ShipType.Recycler]: 20000,
|
||||||
|
[ShipType.EspionageProbe]: 5,
|
||||||
|
[ShipType.SolarSatellite]: 0,
|
||||||
|
[ShipType.DarkMatterHarvester]: 1000,
|
||||||
|
[ShipType.Deathstar]: 1000000
|
||||||
|
}
|
||||||
|
return cargoCapacities[shipType] || 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算行星毁灭概率
|
* 计算行星毁灭概率
|
||||||
*/
|
*/
|
||||||
@@ -692,19 +882,19 @@ export const processDestroyArrival = (
|
|||||||
/**
|
/**
|
||||||
* 处理舰队任务返回
|
* 处理舰队任务返回
|
||||||
*/
|
*/
|
||||||
export const processFleetReturn = (mission: FleetMission, originPlanet: Planet): void => {
|
export const processFleetReturn = (
|
||||||
// 舰船返回
|
mission: FleetMission,
|
||||||
Object.entries(mission.fleet).forEach(([shipType, count]) => {
|
originPlanet: Planet,
|
||||||
if (count > 0) {
|
technologies: Record<TechnologyType, number>,
|
||||||
originPlanet.fleet[shipType as ShipType] += count
|
storageCapacityBonus: number
|
||||||
}
|
): void => {
|
||||||
})
|
// 舰船返回 - 使用安全添加函数
|
||||||
|
fleetStorageLogic.addFleetSafely(originPlanet, mission.fleet, technologies)
|
||||||
|
// 注意:如果舰队仓储溢出,超出部分会丢失(这是合理的惩罚)
|
||||||
|
|
||||||
// 资源返回(掠夺物或运输货物)
|
// 资源返回(掠夺物或运输货物)- 使用安全添加函数
|
||||||
originPlanet.resources.metal += mission.cargo.metal
|
resourceLogic.addResourcesSafely(originPlanet, mission.cargo, storageCapacityBonus)
|
||||||
originPlanet.resources.crystal += mission.cargo.crystal
|
// 注意:如果资源仓储溢出,超出部分会丢失(这是合理的惩罚)
|
||||||
originPlanet.resources.deuterium += mission.cargo.deuterium
|
|
||||||
originPlanet.resources.darkMatter += mission.cargo.darkMatter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -717,7 +907,8 @@ export const updateFleetMissions = async (
|
|||||||
attacker: Player,
|
attacker: Player,
|
||||||
defender: Player | null,
|
defender: Player | null,
|
||||||
now: number,
|
now: number,
|
||||||
allNpcs?: NPC[]
|
allNpcs?: NPC[],
|
||||||
|
locale?: Locale
|
||||||
): Promise<{
|
): Promise<{
|
||||||
completedMissions: string[]
|
completedMissions: string[]
|
||||||
battleReports: BattleResult[]
|
battleReports: BattleResult[]
|
||||||
@@ -741,6 +932,9 @@ export const updateFleetMissions = async (
|
|||||||
|
|
||||||
// 获取所有星球列表(用于月球生成检查)
|
// 获取所有星球列表(用于月球生成检查)
|
||||||
const allPlanets = Array.from(planets.values())
|
const allPlanets = Array.from(planets.values())
|
||||||
|
// 计算军官加成(用于资源容量计算)
|
||||||
|
const bonuses = officerLogic.calculateActiveBonuses(attacker.officers, now)
|
||||||
|
const storageCapacityBonus = bonuses.storageCapacityBonus
|
||||||
|
|
||||||
// 使用 for...of 以支持 await
|
// 使用 for...of 以支持 await
|
||||||
for (const mission of missions) {
|
for (const mission of missions) {
|
||||||
@@ -753,7 +947,7 @@ export const updateFleetMissions = async (
|
|||||||
|
|
||||||
switch (mission.missionType) {
|
switch (mission.missionType) {
|
||||||
case MissionType.Transport:
|
case MissionType.Transport:
|
||||||
processTransportArrival(mission, targetPlanet, attacker, allNpcs)
|
processTransportArrival(mission, targetPlanet, attacker, allNpcs, locale, storageCapacityBonus)
|
||||||
break
|
break
|
||||||
|
|
||||||
case MissionType.Attack: {
|
case MissionType.Attack: {
|
||||||
@@ -767,9 +961,26 @@ export const updateFleetMissions = async (
|
|||||||
planets.set(moonKey, attackResult.moon)
|
planets.set(moonKey, attackResult.moon)
|
||||||
}
|
}
|
||||||
if (attackResult.debrisField) {
|
if (attackResult.debrisField) {
|
||||||
|
// 检查该位置是否已存在残骸场
|
||||||
|
const existingDebris = debrisFields.get(attackResult.debrisField.id)
|
||||||
|
if (existingDebris) {
|
||||||
|
// 累加残骸资源
|
||||||
|
const updatedDebris: DebrisField = {
|
||||||
|
...existingDebris,
|
||||||
|
resources: {
|
||||||
|
metal: existingDebris.resources.metal + attackResult.debrisField.resources.metal,
|
||||||
|
crystal: existingDebris.resources.crystal + attackResult.debrisField.resources.crystal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debrisFields.set(attackResult.debrisField.id, updatedDebris)
|
||||||
|
updatedDebrisFields.push(updatedDebris)
|
||||||
|
} else {
|
||||||
|
// 新建残骸场
|
||||||
|
debrisFields.set(attackResult.debrisField.id, attackResult.debrisField)
|
||||||
newDebrisFields.push(attackResult.debrisField)
|
newDebrisFields.push(attackResult.debrisField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,8 +1000,8 @@ export const updateFleetMissions = async (
|
|||||||
break
|
break
|
||||||
|
|
||||||
case MissionType.Deploy:
|
case MissionType.Deploy:
|
||||||
const deployed = processDeployArrival(mission, targetPlanet, attacker.id)
|
const deployed = processDeployArrival(mission, targetPlanet, attacker.id, attacker.technologies)
|
||||||
if (deployed) {
|
if (deployed.success && !deployed.overflow) {
|
||||||
completedMissions.push(mission.id)
|
completedMissions.push(mission.id)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -821,6 +1032,15 @@ export const updateFleetMissions = async (
|
|||||||
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
if (destroyResult && destroyResult.success && destroyResult.planetId) {
|
||||||
// 星球被摧毁
|
// 星球被摧毁
|
||||||
destroyedPlanetIds.push(destroyResult.planetId)
|
destroyedPlanetIds.push(destroyResult.planetId)
|
||||||
|
|
||||||
|
// 处理外交关系(如果目标是NPC星球)
|
||||||
|
if (targetPlanet && targetPlanet.ownerId && allNpcs && locale) {
|
||||||
|
const planetOwner = allNpcs.find(npc => npc.id === targetPlanet.ownerId)
|
||||||
|
if (planetOwner) {
|
||||||
|
diplomaticLogic.handlePlanetDestructionReputation(attacker, targetPlanet, planetOwner, allNpcs, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
planets.delete(targetKey)
|
planets.delete(targetKey)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -830,7 +1050,7 @@ export const updateFleetMissions = async (
|
|||||||
if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
|
if (mission.status === 'returning' && mission.returnTime && now >= mission.returnTime) {
|
||||||
// 舰队返回
|
// 舰队返回
|
||||||
if (originPlanet) {
|
if (originPlanet) {
|
||||||
processFleetReturn(mission, originPlanet)
|
processFleetReturn(mission, originPlanet, attacker.technologies, storageCapacityBonus)
|
||||||
}
|
}
|
||||||
completedMissions.push(mission.id)
|
completedMissions.push(mission.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ export const calculateMaxFleetStorage = (planet: Planet, technologies: Record<Te
|
|||||||
const shipyardBonus = BUILDINGS[BuildingType.Shipyard].fleetStorageBonus || 0
|
const shipyardBonus = BUILDINGS[BuildingType.Shipyard].fleetStorageBonus || 0
|
||||||
maxStorage += shipyardLevel * shipyardBonus
|
maxStorage += shipyardLevel * shipyardBonus
|
||||||
|
|
||||||
// 3. 计算机技术全局加成
|
// 3. 机库建筑加成(每个星球独立)
|
||||||
|
const hangarLevel = planet.buildings[BuildingType.Hangar] || 0
|
||||||
|
const hangarBonus = BUILDINGS[BuildingType.Hangar].fleetStorageBonus || 0
|
||||||
|
maxStorage += hangarLevel * hangarBonus
|
||||||
|
|
||||||
|
// 4. 计算机技术全局加成
|
||||||
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
|
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
|
||||||
const computerTechBonus = TECHNOLOGIES[TechnologyType.ComputerTechnology].fleetStorageBonus || 0
|
const computerTechBonus = TECHNOLOGIES[TechnologyType.ComputerTechnology].fleetStorageBonus || 0
|
||||||
maxStorage += computerTechLevel * computerTechBonus
|
maxStorage += computerTechLevel * computerTechBonus
|
||||||
@@ -47,6 +52,26 @@ export const calculateMaxFleetStorage = (planet: Planet, technologies: Record<Te
|
|||||||
return maxStorage
|
return maxStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算建造队列中的舰船仓储使用量
|
||||||
|
* @param buildQueue 建造队列
|
||||||
|
* @returns 队列中舰船的仓储使用量
|
||||||
|
*/
|
||||||
|
export const calculateQueueFleetStorageUsage = (buildQueue: Array<{ type: string; itemType: string; quantity?: number }>): number => {
|
||||||
|
let queueUsage = 0
|
||||||
|
|
||||||
|
for (const item of buildQueue) {
|
||||||
|
if (item.type === 'ship') {
|
||||||
|
const shipType = item.itemType as ShipType
|
||||||
|
const quantity = item.quantity || 0
|
||||||
|
const shipConfig = SHIPS[shipType]
|
||||||
|
queueUsage += quantity * shipConfig.storageUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queueUsage
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否有足够的舰队仓储空间建造新舰船
|
* 检查是否有足够的舰队仓储空间建造新舰船
|
||||||
* @param planet 星球对象
|
* @param planet 星球对象
|
||||||
@@ -62,10 +87,11 @@ export const hasEnoughFleetStorage = (
|
|||||||
technologies: Record<TechnologyType, number>
|
technologies: Record<TechnologyType, number>
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const currentUsage = calculateFleetStorageUsage(planet.fleet)
|
const currentUsage = calculateFleetStorageUsage(planet.fleet)
|
||||||
|
const queueUsage = calculateQueueFleetStorageUsage(planet.buildQueue)
|
||||||
const maxStorage = calculateMaxFleetStorage(planet, technologies)
|
const maxStorage = calculateMaxFleetStorage(planet, technologies)
|
||||||
const newShipUsage = SHIPS[shipType].storageUsage * quantity
|
const newShipUsage = SHIPS[shipType].storageUsage * quantity
|
||||||
|
|
||||||
return currentUsage + newShipUsage <= maxStorage
|
return currentUsage + queueUsage + newShipUsage <= maxStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,5 +108,52 @@ export const getMaxBuildableShips = (planet: Planet, shipType: ShipType, technol
|
|||||||
const shipStorageUsage = SHIPS[shipType].storageUsage
|
const shipStorageUsage = SHIPS[shipType].storageUsage
|
||||||
|
|
||||||
if (shipStorageUsage === 0) return Number.MAX_SAFE_INTEGER
|
if (shipStorageUsage === 0) return Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
|
// 如果当前已经超限(舰队返回等情况),则不允许建造新舰船
|
||||||
|
if (availableStorage <= 0) return 0
|
||||||
|
|
||||||
return Math.floor(availableStorage / shipStorageUsage)
|
return Math.floor(availableStorage / shipStorageUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地添加舰船到星球(会检查舰队仓储容量上限)
|
||||||
|
* @param planet 星球对象
|
||||||
|
* @param fleet 要添加的舰船
|
||||||
|
* @param technologies 玩家的科技等级
|
||||||
|
* @returns 实际添加的舰船数量和溢出的舰船数量
|
||||||
|
*/
|
||||||
|
export const addFleetSafely = (
|
||||||
|
planet: Planet,
|
||||||
|
fleet: Partial<Record<ShipType, number>>,
|
||||||
|
technologies: Record<TechnologyType, number>
|
||||||
|
): { added: Partial<Record<ShipType, number>>; overflow: Partial<Record<ShipType, number>> } => {
|
||||||
|
const maxStorage = calculateMaxFleetStorage(planet, technologies)
|
||||||
|
let currentUsage = calculateFleetStorageUsage(planet.fleet)
|
||||||
|
|
||||||
|
const added: Partial<Record<ShipType, number>> = {}
|
||||||
|
const overflow: Partial<Record<ShipType, number>> = {}
|
||||||
|
|
||||||
|
for (const [shipType, count] of Object.entries(fleet)) {
|
||||||
|
if (count <= 0) continue
|
||||||
|
|
||||||
|
const ship = shipType as ShipType
|
||||||
|
const shipStorageUsage = SHIPS[ship].storageUsage
|
||||||
|
|
||||||
|
// 计算可以添加多少艘(不超过容量上限)
|
||||||
|
const spaceAvailable = Math.max(0, maxStorage - currentUsage)
|
||||||
|
const maxCanAdd = shipStorageUsage > 0 ? Math.floor(spaceAvailable / shipStorageUsage) : count
|
||||||
|
const actuallyAdded = Math.min(count, maxCanAdd)
|
||||||
|
const overflowed = count - actuallyAdded
|
||||||
|
if (actuallyAdded > 0) {
|
||||||
|
planet.fleet[ship] = (planet.fleet[ship] || 0) + actuallyAdded
|
||||||
|
added[ship] = actuallyAdded
|
||||||
|
currentUsage += actuallyAdded * shipStorageUsage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowed > 0) {
|
||||||
|
overflow[ship] = overflowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { added, overflow }
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const initializePlayer = (playerId: string, playerName: string = 'Command
|
|||||||
incomingFleetAlerts: [],
|
incomingFleetAlerts: [],
|
||||||
giftNotifications: [],
|
giftNotifications: [],
|
||||||
giftRejectedNotifications: [],
|
giftRejectedNotifications: [],
|
||||||
diplomaticRelations: {},
|
|
||||||
diplomaticReports: [],
|
diplomaticReports: [],
|
||||||
points: 0
|
points: 0
|
||||||
}
|
}
|
||||||
@@ -100,7 +99,9 @@ export const generatePositionKey = (galaxy: number, system: number, position: nu
|
|||||||
*/
|
*/
|
||||||
export const processGameUpdate = (
|
export const processGameUpdate = (
|
||||||
player: Player,
|
player: Player,
|
||||||
now: number
|
now: number,
|
||||||
|
gameSpeed: number = 1,
|
||||||
|
onNotification?: (type: string, itemType: string, level?: number) => void
|
||||||
): {
|
): {
|
||||||
updatedResearchQueue: BuildQueueItem[]
|
updatedResearchQueue: BuildQueueItem[]
|
||||||
} => {
|
} => {
|
||||||
@@ -112,15 +113,22 @@ export const processGameUpdate = (
|
|||||||
pointsLogic.addPoints(player, points)
|
pointsLogic.addPoints(player, points)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知回调
|
||||||
|
const onCompleted = (type: string, itemType: string, level?: number, _quantity?: number) => {
|
||||||
|
if (onNotification) {
|
||||||
|
onNotification(type, itemType, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
|
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
|
||||||
player.planets.forEach(planet => {
|
player.planets.forEach(planet => {
|
||||||
resourceLogic.updatePlanetResources(planet, now, bonuses)
|
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新所有星球其他状态
|
// 更新所有星球其他状态
|
||||||
player.planets.forEach(planet => {
|
player.planets.forEach(planet => {
|
||||||
// 检查建造队列
|
// 检查建造队列
|
||||||
buildingLogic.completeBuildQueue(planet, now, onPointsEarned)
|
buildingLogic.completeBuildQueue(planet, now, onPointsEarned, onCompleted)
|
||||||
|
|
||||||
// 更新星球最大空间
|
// 更新星球最大空间
|
||||||
if (planet.isMoon) {
|
if (planet.isMoon) {
|
||||||
@@ -132,7 +140,13 @@ export const processGameUpdate = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 检查研究队列
|
// 检查研究队列
|
||||||
const updatedResearchQueue = researchLogic.completeResearchQueue(player.researchQueue, player.technologies, now, onPointsEarned)
|
const updatedResearchQueue = researchLogic.completeResearchQueue(
|
||||||
|
player.researchQueue,
|
||||||
|
player.technologies,
|
||||||
|
now,
|
||||||
|
onPointsEarned,
|
||||||
|
onCompleted
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updatedResearchQueue
|
updatedResearchQueue
|
||||||
|
|||||||
@@ -31,11 +31,7 @@ export const calculateSystemDistance = (from: Position, to: Position): number =>
|
|||||||
/**
|
/**
|
||||||
* 检查目标是否在射程内
|
* 检查目标是否在射程内
|
||||||
*/
|
*/
|
||||||
export const isTargetInRange = (
|
export const isTargetInRange = (originPosition: Position, targetPosition: Position, impulseDriveLevel: number): boolean => {
|
||||||
originPosition: Position,
|
|
||||||
targetPosition: Position,
|
|
||||||
impulseDriveLevel: number
|
|
||||||
): boolean => {
|
|
||||||
const range = calculateMissileRange(impulseDriveLevel)
|
const range = calculateMissileRange(impulseDriveLevel)
|
||||||
const distance = calculateSystemDistance(originPosition, targetPosition)
|
const distance = calculateSystemDistance(originPosition, targetPosition)
|
||||||
return distance <= range
|
return distance <= range
|
||||||
@@ -156,11 +152,7 @@ export const calculateMissileImpact = (
|
|||||||
const defenseTypes = Object.keys(defenderPlanet.defense) as DefenseType[]
|
const defenseTypes = Object.keys(defenderPlanet.defense) as DefenseType[]
|
||||||
const availableDefenses = defenseTypes.filter(type => {
|
const availableDefenses = defenseTypes.filter(type => {
|
||||||
// 不能摧毁护盾罩和行星护盾
|
// 不能摧毁护盾罩和行星护盾
|
||||||
if (
|
if (type === DefenseTypes.SmallShieldDome || type === DefenseTypes.LargeShieldDome || type === DefenseTypes.PlanetaryShield) {
|
||||||
type === DefenseTypes.SmallShieldDome ||
|
|
||||||
type === DefenseTypes.LargeShieldDome ||
|
|
||||||
type === DefenseTypes.PlanetaryShield
|
|
||||||
) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return (defenderPlanet.defense[type] || 0) > 0
|
return (defenderPlanet.defense[type] || 0) > 0
|
||||||
@@ -196,10 +188,7 @@ export const calculateMissileImpact = (
|
|||||||
/**
|
/**
|
||||||
* 应用导弹攻击结果到星球
|
* 应用导弹攻击结果到星球
|
||||||
*/
|
*/
|
||||||
export const applyMissileAttackResult = (
|
export const applyMissileAttackResult = (planet: Planet, defenseLosses: Partial<Record<DefenseType, number>>): void => {
|
||||||
planet: Planet,
|
|
||||||
defenseLosses: Partial<Record<DefenseType, number>>
|
|
||||||
): void => {
|
|
||||||
for (const [defenseType, lossCount] of Object.entries(defenseLosses)) {
|
for (const [defenseType, lossCount] of Object.entries(defenseLosses)) {
|
||||||
const currentCount = planet.defense[defenseType as DefenseType] || 0
|
const currentCount = planet.defense[defenseType as DefenseType] || 0
|
||||||
planet.defense[defenseType as DefenseType] = Math.max(0, currentCount - lossCount)
|
planet.defense[defenseType as DefenseType] = Math.max(0, currentCount - lossCount)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { NPC, Planet, Player, FleetMission, SpyReport, SpiedNotification, I
|
|||||||
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
import { MissionType, ShipType, TechnologyType, RelationStatus } from '@/types/game'
|
||||||
import * as fleetLogic from './fleetLogic'
|
import * as fleetLogic from './fleetLogic'
|
||||||
import * as diplomaticLogic from './diplomaticLogic'
|
import * as diplomaticLogic from './diplomaticLogic'
|
||||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
import { DIPLOMATIC_CONFIG, SHIPS } from '@/config/gameConfig'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NPC行为决策系统
|
* NPC行为决策系统
|
||||||
@@ -23,6 +23,8 @@ export interface DynamicBehaviorConfig {
|
|||||||
attackProbability: number
|
attackProbability: number
|
||||||
minSpyProbes: number
|
minSpyProbes: number
|
||||||
attackFleetSizeRatio: number
|
attackFleetSizeRatio: number
|
||||||
|
maxConcurrentSpyMissions: number // 同时最多多少个侦查任务
|
||||||
|
maxConcurrentAttackMissions: number // 同时最多多少个攻击任务
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,49 +33,59 @@ export interface DynamicBehaviorConfig {
|
|||||||
*/
|
*/
|
||||||
export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorConfig => {
|
export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorConfig => {
|
||||||
if (playerPoints < 1000) {
|
if (playerPoints < 1000) {
|
||||||
// 新手阶段:NPC很温和
|
// 新手阶段:NPC温和但会主动侦查攻击
|
||||||
return {
|
return {
|
||||||
spyInterval: 2400, // 40分钟侦查一次
|
spyInterval: 300, // 5分钟侦查一次
|
||||||
attackInterval: 4800, // 80分钟攻击一次
|
attackInterval: 300, // 5分钟攻击一次(与侦查同步,侦查完就攻击)
|
||||||
attackProbability: 0.15, // 15%概率攻击
|
attackProbability: 0.4,
|
||||||
minSpyProbes: 1,
|
minSpyProbes: 1,
|
||||||
attackFleetSizeRatio: 0.3 // 只派30%舰队
|
attackFleetSizeRatio: 0.3, // 只派30%舰队
|
||||||
|
maxConcurrentSpyMissions: 3,
|
||||||
|
maxConcurrentAttackMissions: 2
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 5000) {
|
} else if (playerPoints < 5000) {
|
||||||
// 初级阶段:NPC稍微激进
|
// 初级阶段:NPC比较激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 1800, // 30分钟侦查一次
|
spyInterval: 420, // 7分钟侦查一次
|
||||||
attackInterval: 3600, // 60分钟攻击一次
|
attackInterval: 420, // 7分钟攻击一次(与侦查同步)
|
||||||
attackProbability: 0.25, // 25%概率攻击
|
attackProbability: 0.45,
|
||||||
minSpyProbes: 2,
|
minSpyProbes: 2,
|
||||||
attackFleetSizeRatio: 0.5 // 派50%舰队
|
attackFleetSizeRatio: 0.5, // 派50%舰队
|
||||||
|
maxConcurrentSpyMissions: 5,
|
||||||
|
maxConcurrentAttackMissions: 3
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 20000) {
|
} else if (playerPoints < 20000) {
|
||||||
// 中级阶段:NPC比较激进
|
// 中级阶段:NPC很激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 1200, // 20分钟侦查一次
|
spyInterval: 360, // 6分钟侦查一次
|
||||||
attackInterval: 2400, // 40分钟攻击一次
|
attackInterval: 360, // 6分钟攻击一次(与侦查同步)
|
||||||
attackProbability: 0.4, // 40%概率攻击
|
attackProbability: 0.55,
|
||||||
minSpyProbes: 3,
|
minSpyProbes: 3,
|
||||||
attackFleetSizeRatio: 0.7 // 派70%舰队
|
attackFleetSizeRatio: 0.7, // 派70%舰队
|
||||||
|
maxConcurrentSpyMissions: 8,
|
||||||
|
maxConcurrentAttackMissions: 5
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 50000) {
|
} else if (playerPoints < 50000) {
|
||||||
// 高级阶段:NPC很激进
|
// 高级阶段:NPC非常激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 900, // 15分钟侦查一次
|
spyInterval: 300, // 5分钟侦查一次
|
||||||
attackInterval: 1800, // 30分钟攻击一次
|
attackInterval: 300, // 5分钟攻击一次(与侦查同步)
|
||||||
attackProbability: 0.55, // 55%概率攻击
|
attackProbability: 0.65,
|
||||||
minSpyProbes: 4,
|
minSpyProbes: 4,
|
||||||
attackFleetSizeRatio: 0.85 // 派85%舰队
|
attackFleetSizeRatio: 0.85, // 派85%舰队
|
||||||
|
maxConcurrentSpyMissions: 10,
|
||||||
|
maxConcurrentAttackMissions: 8
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 专家阶段:NPC非常激进
|
// 专家阶段:NPC极度激进
|
||||||
return {
|
return {
|
||||||
spyInterval: 600, // 10分钟侦查一次
|
spyInterval: 240, // 4分钟侦查一次
|
||||||
attackInterval: 1200, // 20分钟攻击一次
|
attackInterval: 240, // 4分钟攻击一次(与侦查同步)
|
||||||
attackProbability: 0.7, // 70%概率攻击
|
attackProbability: 0.8,
|
||||||
minSpyProbes: 5,
|
minSpyProbes: 5,
|
||||||
attackFleetSizeRatio: 0.95 // 派95%舰队
|
attackFleetSizeRatio: 0.95, // 派95%舰队
|
||||||
|
maxConcurrentSpyMissions: 15,
|
||||||
|
maxConcurrentAttackMissions: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,26 +94,37 @@ export const calculateDynamicBehavior = (playerPoints: number): DynamicBehaviorC
|
|||||||
* 检查NPC是否应该侦查玩家
|
* 检查NPC是否应该侦查玩家
|
||||||
*/
|
*/
|
||||||
export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
||||||
const lastSpyTime = npc.lastSpyTime || 0
|
// 新手保护:积分低于1000的玩家不会被侦查
|
||||||
|
const playerPoints = player.points || 0
|
||||||
|
if (playerPoints < 1000) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否达到侦查间隔
|
// 检查外交关系 - 统一使用 npc.relations
|
||||||
|
const relation = npc.relations?.[player.id]
|
||||||
|
|
||||||
|
// 如果没有关系数据,视为中立,不侦查
|
||||||
|
if (!relation) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 友好或中立NPC不侦查
|
||||||
|
if (relation.status === RelationStatus.Friendly || relation.status === RelationStatus.Neutral) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有敌对NPC才会继续
|
||||||
|
if (relation.status !== RelationStatus.Hostile) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有敌对NPC才会到达这里,检查冷却时间
|
||||||
|
const lastSpyTime = npc.lastSpyTime || 0
|
||||||
if (currentTime - lastSpyTime < config.spyInterval * 1000) {
|
if (currentTime - lastSpyTime < config.spyInterval * 1000) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查外交关系 - 根据关系状态调整侦查概率
|
// 敌对NPC且冷却结束,执行侦查
|
||||||
const relation = npc.relations?.[player.id]
|
|
||||||
if (relation) {
|
|
||||||
if (relation.status === RelationStatus.Friendly) {
|
|
||||||
// 友好NPC侦查频率降低到50%
|
|
||||||
return Math.random() < 0.5
|
|
||||||
}
|
|
||||||
if (relation.status === RelationStatus.Hostile) {
|
|
||||||
// 敌对NPC必定侦查
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,28 +132,44 @@ export const shouldNPCSpyPlayer = (npc: NPC, player: Player, currentTime: number
|
|||||||
* 检查NPC是否应该攻击玩家
|
* 检查NPC是否应该攻击玩家
|
||||||
*/
|
*/
|
||||||
export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
export const shouldNPCAttackPlayer = (npc: NPC, player: Player, currentTime: number, config: DynamicBehaviorConfig): boolean => {
|
||||||
const lastAttackTime = npc.lastAttackTime || 0
|
// 新手保护:积分低于1000的玩家不会被攻击
|
||||||
|
const playerPoints = player.points || 0
|
||||||
|
if (playerPoints < 1000) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否达到攻击间隔
|
// 检查外交关系 - 统一使用 npc.relations
|
||||||
|
const relation = npc.relations?.[player.id]
|
||||||
|
|
||||||
|
// 如果没有关系数据,视为中立,不攻击
|
||||||
|
if (!relation) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 友好或中立NPC不攻击
|
||||||
|
if (relation.status === RelationStatus.Friendly || relation.status === RelationStatus.Neutral) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有敌对NPC才会继续
|
||||||
|
if (relation.status !== RelationStatus.Hostile) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查攻击冷却
|
||||||
|
const lastAttackTime = npc.lastAttackTime || 0
|
||||||
if (currentTime - lastAttackTime < config.attackInterval * 1000) {
|
if (currentTime - lastAttackTime < config.attackInterval * 1000) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查外交关系
|
// 必须有侦查报告才能攻击
|
||||||
const relation = npc.relations?.[player.id]
|
if (!npc.playerSpyReports || Object.keys(npc.playerSpyReports).length === 0) {
|
||||||
if (relation) {
|
|
||||||
if (relation.status === RelationStatus.Friendly) {
|
|
||||||
// 友好NPC不攻击玩家
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (relation.status === RelationStatus.Hostile) {
|
|
||||||
// 敌对NPC攻击概率翻倍
|
|
||||||
return Math.random() < config.attackProbability * 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中立或无关系:正常概率
|
// 有侦查报告的情况下,敌对NPC一定会攻击(移除概率限制)
|
||||||
return Math.random() < config.attackProbability
|
// 这样保证侦查后会跟进攻击,而不是无意义地反复侦查
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +189,7 @@ export const shouldNPCGiftPlayer = (npc: NPC, player: Player, currentTime: numbe
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查NPC对玩家的好感度
|
// 检查好感度 - 统一使用 npc.relations
|
||||||
const relation = npc.relations?.[player.id]
|
const relation = npc.relations?.[player.id]
|
||||||
if (!relation || relation.reputation < NPC_GIFT_CONFIG.MIN_REPUTATION) {
|
if (!relation || relation.reputation < NPC_GIFT_CONFIG.MIN_REPUTATION) {
|
||||||
return false
|
return false
|
||||||
@@ -211,6 +250,35 @@ const selectBestNPCPlanet = (npc: NPC, targetPosition: { galaxy: number; system:
|
|||||||
return bestPlanet
|
return bestPlanet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算NPC星球的战斗舰队总攻击力
|
||||||
|
* 用于判断NPC是否有足够的战斗力来发起有意义的攻击
|
||||||
|
*/
|
||||||
|
const calculateNPCCombatPower = (npcPlanet: Planet): number => {
|
||||||
|
// 各舰船的攻击力
|
||||||
|
const shipAttackPower: Record<string, number> = {
|
||||||
|
[ShipType.LightFighter]: 50,
|
||||||
|
[ShipType.HeavyFighter]: 150,
|
||||||
|
[ShipType.Cruiser]: 400,
|
||||||
|
[ShipType.Battleship]: 1200,
|
||||||
|
[ShipType.Bomber]: 700,
|
||||||
|
[ShipType.Destroyer]: 2500,
|
||||||
|
[ShipType.Battlecruiser]: 1000,
|
||||||
|
[ShipType.Deathstar]: 200000
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalPower = 0
|
||||||
|
for (const [shipType, attack] of Object.entries(shipAttackPower)) {
|
||||||
|
const count = npcPlanet.fleet[shipType as ShipType] || 0
|
||||||
|
totalPower += count * attack
|
||||||
|
}
|
||||||
|
return totalPower
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最小战斗力阈值:相当于约10艘轻型战斗机的攻击力
|
||||||
|
// 这样避免NPC只有几艘小飞机就频繁侦查骚扰玩家
|
||||||
|
const MIN_COMBAT_POWER_FOR_SPY = 500
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建NPC侦查任务
|
* 创建NPC侦查任务
|
||||||
*/
|
*/
|
||||||
@@ -232,6 +300,13 @@ export const createNPCSpyMission = (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查NPC是否有足够的战斗力
|
||||||
|
// 战斗力太低的话侦查没有意义,也避免频繁骚扰玩家
|
||||||
|
const combatPower = calculateNPCCombatPower(npcPlanet)
|
||||||
|
if (combatPower < MIN_COMBAT_POWER_FOR_SPY) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// 创建侦查舰队
|
// 创建侦查舰队
|
||||||
const fleet: Partial<Fleet> = {
|
const fleet: Partial<Fleet> = {
|
||||||
[ShipType.EspionageProbe]: config.minSpyProbes
|
[ShipType.EspionageProbe]: config.minSpyProbes
|
||||||
@@ -359,13 +434,13 @@ const decideAttackFleet = (_npc: NPC, npcPlanet: Planet, _spyReport: SpyReport,
|
|||||||
for (const shipType of combatShips) {
|
for (const shipType of combatShips) {
|
||||||
const available = npcPlanet.fleet[shipType] || 0
|
const available = npcPlanet.fleet[shipType] || 0
|
||||||
if (available > 0) {
|
if (available > 0) {
|
||||||
const sendCount = Math.floor(available * config.attackFleetSizeRatio)
|
// 使用 Math.ceil 确保至少派出1艘(如果有的话)
|
||||||
if (sendCount > 0) {
|
// 但不能超过可用数量
|
||||||
|
const sendCount = Math.min(available, Math.max(1, Math.floor(available * config.attackFleetSizeRatio)))
|
||||||
attackFleet[shipType] = sendCount
|
attackFleet[shipType] = sendCount
|
||||||
hasShips = true
|
hasShips = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return hasShips ? attackFleet : null
|
return hasShips ? attackFleet : null
|
||||||
}
|
}
|
||||||
@@ -581,6 +656,111 @@ export const updateNPCBehavior = (
|
|||||||
updateIncomingFleetAlerts(player, currentTime)
|
updateIncomingFleetAlerts(player, currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带并发限制的NPC行为更新函数
|
||||||
|
* 防止同时产生过多侦查和攻击任务导致游戏卡顿
|
||||||
|
*/
|
||||||
|
export const updateNPCBehaviorWithLimit = (
|
||||||
|
npc: NPC,
|
||||||
|
player: Player,
|
||||||
|
allPlanets: Planet[],
|
||||||
|
debrisFields: Record<string, DebrisField>,
|
||||||
|
currentTime: number,
|
||||||
|
limits: {
|
||||||
|
activeSpyMissions: number
|
||||||
|
activeAttackMissions: number
|
||||||
|
config: DynamicBehaviorConfig
|
||||||
|
}
|
||||||
|
): { spyCreated: boolean; attackCreated: boolean } => {
|
||||||
|
const { activeSpyMissions, activeAttackMissions, config } = limits
|
||||||
|
let spyCreated = false
|
||||||
|
let attackCreated = false
|
||||||
|
|
||||||
|
// 1. 检查并回收附近的残骸(优先级最高,不受并发限制)
|
||||||
|
const nearbyDebris = findNearbyDebris(npc, debrisFields)
|
||||||
|
if (nearbyDebris.length > 0) {
|
||||||
|
const activeRecycleMissions = npc.fleetMissions?.filter(m => m.missionType === MissionType.Recycle && m.status === 'outbound') || []
|
||||||
|
const activeDebrisIds = new Set(activeRecycleMissions.map(m => m.debrisFieldId).filter(Boolean))
|
||||||
|
const availableDebris = nearbyDebris.filter(d => !activeDebrisIds.has(d.id))
|
||||||
|
|
||||||
|
if (availableDebris.length > 0) {
|
||||||
|
const targetDebris = availableDebris[Math.floor(Math.random() * availableDebris.length)]
|
||||||
|
if (targetDebris) {
|
||||||
|
createNPCRecycleMission(npc, targetDebris, player, allPlanets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否应该反击(优先于普通攻击,受攻击并发限制)
|
||||||
|
if (activeAttackMissions < config.maxConcurrentAttackMissions && shouldNPCRevenge(npc, currentTime)) {
|
||||||
|
const revengeMission = createNPCRevengeMission(npc, allPlanets, config)
|
||||||
|
if (revengeMission) {
|
||||||
|
const targetPlanet = allPlanets.find(p => p.id === revengeMission.targetPlanetId)
|
||||||
|
if (targetPlanet) {
|
||||||
|
const alert = createIncomingFleetAlert(revengeMission, npc, targetPlanet)
|
||||||
|
if (!player.incomingFleetAlerts) {
|
||||||
|
player.incomingFleetAlerts = []
|
||||||
|
}
|
||||||
|
player.incomingFleetAlerts.push(alert)
|
||||||
|
attackCreated = true
|
||||||
|
}
|
||||||
|
return { spyCreated, attackCreated }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否应该侦查玩家(受侦查并发限制)
|
||||||
|
if (activeSpyMissions < config.maxConcurrentSpyMissions && shouldNPCSpyPlayer(npc, player, currentTime, config)) {
|
||||||
|
const playerPlanets = allPlanets.filter(p => p.ownerId === player.id)
|
||||||
|
if (playerPlanets.length > 0) {
|
||||||
|
const targetPlanet = playerPlanets[Math.floor(Math.random() * playerPlanets.length)]
|
||||||
|
if (targetPlanet) {
|
||||||
|
const spyMission = createNPCSpyMission(npc, targetPlanet, allPlanets, config)
|
||||||
|
if (spyMission) {
|
||||||
|
const alert = createIncomingFleetAlert(spyMission, npc, targetPlanet)
|
||||||
|
if (!player.incomingFleetAlerts) {
|
||||||
|
player.incomingFleetAlerts = []
|
||||||
|
}
|
||||||
|
player.incomingFleetAlerts.push(alert)
|
||||||
|
spyCreated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查是否应该攻击玩家(受攻击并发限制)
|
||||||
|
if (activeAttackMissions < config.maxConcurrentAttackMissions && shouldNPCAttackPlayer(npc, player, currentTime, config)) {
|
||||||
|
if (npc.playerSpyReports && Object.keys(npc.playerSpyReports).length > 0) {
|
||||||
|
const spyReports = Object.values(npc.playerSpyReports)
|
||||||
|
const recentReport = spyReports[Math.floor(Math.random() * spyReports.length)]
|
||||||
|
|
||||||
|
if (recentReport) {
|
||||||
|
const targetPlanet = allPlanets.find(p => p.id === recentReport.targetPlanetId)
|
||||||
|
if (targetPlanet) {
|
||||||
|
const attackMission = createNPCAttackMission(npc, targetPlanet, recentReport, config)
|
||||||
|
if (attackMission) {
|
||||||
|
const alert = createIncomingFleetAlert(attackMission, npc, targetPlanet)
|
||||||
|
if (!player.incomingFleetAlerts) {
|
||||||
|
player.incomingFleetAlerts = []
|
||||||
|
}
|
||||||
|
player.incomingFleetAlerts.push(alert)
|
||||||
|
attackCreated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 检查是否应该赠送资源给玩家(仅友好NPC,不受并发限制)
|
||||||
|
if (shouldNPCGiftPlayer(npc, player, currentTime)) {
|
||||||
|
giftResourcesToPlayer(npc, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新即将到来的舰队警告(删除过期的)
|
||||||
|
updateIncomingFleetAlerts(player, currentTime)
|
||||||
|
|
||||||
|
return { spyCreated, attackCreated }
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 测试辅助函数 ==========
|
// ========== 测试辅助函数 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1059,3 +1239,156 @@ export const createNPCRevengeMission = (npc: NPC, allPlanets: Planet[], config:
|
|||||||
|
|
||||||
return mission
|
return mission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NPC状态诊断函数 - 用于调试和了解NPC当前状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 诊断原因类型,包含翻译键和参数
|
||||||
|
export interface DiagnosticReason {
|
||||||
|
key: string // 翻译键,如 'friendlyNoAction', 'insufficientProbes'
|
||||||
|
params?: Record<string, string | number> // 翻译参数,如 { current: 5, required: 10 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关系状态翻译键类型
|
||||||
|
export type RelationStatusKey = 'friendly' | 'hostile' | 'neutral' | 'noRelation' | 'noRelationNeutral'
|
||||||
|
|
||||||
|
export interface NPCDiagnosticInfo {
|
||||||
|
npcId: string
|
||||||
|
npcName: string
|
||||||
|
difficulty: string
|
||||||
|
relationStatus: string // 保持原有字段用于显示
|
||||||
|
relationStatusKey: RelationStatusKey // 翻译键
|
||||||
|
reputation: number
|
||||||
|
canSpy: boolean
|
||||||
|
canAttack: boolean
|
||||||
|
spyProbes: number
|
||||||
|
totalFleetPower: number
|
||||||
|
lastSpyTime: number
|
||||||
|
lastAttackTime: number
|
||||||
|
timeSinceLastSpy: number
|
||||||
|
timeSinceLastAttack: number
|
||||||
|
nextSpyIn: number
|
||||||
|
nextAttackIn: number
|
||||||
|
attackProbability: number
|
||||||
|
reasons: DiagnosticReason[] // 改为结构化原因数组
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diagnoseNPCBehavior = (npcs: NPC[], player: Player, currentTime: number): NPCDiagnosticInfo[] => {
|
||||||
|
const playerPoints = player.points || 0
|
||||||
|
const config = calculateDynamicBehavior(playerPoints)
|
||||||
|
|
||||||
|
return npcs.map(npc => {
|
||||||
|
const planet = npc.planets[0]
|
||||||
|
const relation = npc.relations?.[player.id]
|
||||||
|
const reasons: DiagnosticReason[] = []
|
||||||
|
|
||||||
|
// 检查外交关系
|
||||||
|
let canSpy = false // 默认不能侦查,只有敌对NPC才能侦查
|
||||||
|
let canAttack = false // 默认不能攻击,只有敌对NPC才能攻击
|
||||||
|
let relationStatus = ''
|
||||||
|
let relationStatusKey: RelationStatusKey = 'noRelation'
|
||||||
|
let reputation = 0
|
||||||
|
|
||||||
|
if (relation) {
|
||||||
|
reputation = relation.reputation || 0
|
||||||
|
|
||||||
|
if (relation.status === RelationStatus.Friendly) {
|
||||||
|
relationStatus = 'friendly'
|
||||||
|
relationStatusKey = 'friendly'
|
||||||
|
reasons.push({ key: 'friendlyNoAction' })
|
||||||
|
} else if (relation.status === RelationStatus.Neutral) {
|
||||||
|
relationStatus = 'neutral'
|
||||||
|
relationStatusKey = 'neutral'
|
||||||
|
reasons.push({ key: 'neutralNoAction' })
|
||||||
|
} else if (relation.status === RelationStatus.Hostile) {
|
||||||
|
relationStatus = 'hostile'
|
||||||
|
relationStatusKey = 'hostile'
|
||||||
|
canSpy = true
|
||||||
|
canAttack = true
|
||||||
|
reasons.push({ key: 'hostileWillAct' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无关系的NPC视为中立
|
||||||
|
relationStatus = 'noRelationNeutral'
|
||||||
|
relationStatusKey = 'noRelationNeutral'
|
||||||
|
reasons.push({ key: 'noRelationNeutral' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查侦查探测器数量
|
||||||
|
const spyProbes = planet?.fleet?.[ShipType.EspionageProbe] || 0
|
||||||
|
if (spyProbes < config.minSpyProbes) {
|
||||||
|
canSpy = false
|
||||||
|
reasons.push({ key: 'insufficientProbes', params: { current: spyProbes, required: config.minSpyProbes } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算舰队战力
|
||||||
|
let totalFleetPower = 0
|
||||||
|
if (planet?.fleet) {
|
||||||
|
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||||
|
const shipConfig = SHIPS[shipType as ShipType]
|
||||||
|
if (shipConfig) {
|
||||||
|
const power = shipConfig.attack + shipConfig.shield + shipConfig.armor / 10
|
||||||
|
totalFleetPower += power * (count as number)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFleetPower === 0) {
|
||||||
|
canAttack = false
|
||||||
|
reasons.push({ key: 'noFleet' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间检查
|
||||||
|
const lastSpyTime = npc.lastSpyTime || 0
|
||||||
|
const lastAttackTime = npc.lastAttackTime || 0
|
||||||
|
const timeSinceLastSpy = Math.floor((currentTime - lastSpyTime) / 1000)
|
||||||
|
const timeSinceLastAttack = Math.floor((currentTime - lastAttackTime) / 1000)
|
||||||
|
|
||||||
|
const nextSpyIn = Math.max(0, config.spyInterval - timeSinceLastSpy)
|
||||||
|
const nextAttackIn = Math.max(0, config.attackInterval - timeSinceLastAttack)
|
||||||
|
|
||||||
|
if (timeSinceLastSpy < config.spyInterval) {
|
||||||
|
reasons.push({ key: 'spyCooldown', params: { min: Math.floor(nextSpyIn / 60), sec: nextSpyIn % 60 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSinceLastAttack < config.attackInterval) {
|
||||||
|
reasons.push({ key: 'attackCooldown', params: { min: Math.floor(nextAttackIn / 60), sec: nextAttackIn % 60 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经侦查过玩家
|
||||||
|
const hasSpiedPlayer = npc.playerSpyReports && Object.keys(npc.playerSpyReports).length > 0
|
||||||
|
|
||||||
|
if (!hasSpiedPlayer && canAttack) {
|
||||||
|
canAttack = false
|
||||||
|
reasons.push({ key: 'notSpiedYet' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际攻击概率
|
||||||
|
let actualAttackProbability = config.attackProbability
|
||||||
|
if (relation?.status === RelationStatus.Hostile) {
|
||||||
|
actualAttackProbability = Math.min(config.attackProbability * 2.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
npcId: npc.id,
|
||||||
|
npcName: npc.name,
|
||||||
|
difficulty: npc.difficulty,
|
||||||
|
relationStatus,
|
||||||
|
relationStatusKey,
|
||||||
|
reputation,
|
||||||
|
canSpy,
|
||||||
|
canAttack,
|
||||||
|
spyProbes,
|
||||||
|
totalFleetPower: Math.floor(totalFleetPower),
|
||||||
|
lastSpyTime,
|
||||||
|
lastAttackTime,
|
||||||
|
timeSinceLastSpy,
|
||||||
|
timeSinceLastAttack,
|
||||||
|
nextSpyIn,
|
||||||
|
nextAttackIn,
|
||||||
|
attackProbability: actualAttackProbability,
|
||||||
|
reasons
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficu
|
|||||||
// 积分区间和对应的难度参数
|
// 积分区间和对应的难度参数
|
||||||
if (playerPoints < 1000) {
|
if (playerPoints < 1000) {
|
||||||
// 新手期:0-1,000分
|
// 新手期:0-1,000分
|
||||||
// NPC保持30-50%实力,给予充分发展空间
|
// NPC保持30-50%实力,给予充分发展空间,但资源增长速度加快
|
||||||
const ratio = 0.3 + (playerPoints / 1000) * 0.2
|
const ratio = 0.3 + (playerPoints / 1000) * 0.2
|
||||||
return {
|
return {
|
||||||
powerRatio: ratio,
|
powerRatio: ratio,
|
||||||
checkInterval: 300, // 5分钟
|
checkInterval: 300, // 5分钟
|
||||||
resourceGrowthRate: 0.4,
|
resourceGrowthRate: 0.8, // 从0.4提升到0.8,确保NPC有足够资源发育
|
||||||
buildingGrowthSpeed: 0.4,
|
buildingGrowthSpeed: 0.6, // 从0.4提升到0.6
|
||||||
techGrowthSpeed: 0.4
|
techGrowthSpeed: 0.6 // 从0.4提升到0.6
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 5000) {
|
} else if (playerPoints < 5000) {
|
||||||
// 初级期:1,000-5,000分
|
// 初级期:1,000-5,000分
|
||||||
@@ -77,9 +77,9 @@ export const calculateDynamicDifficulty = (playerPoints: number): DynamicDifficu
|
|||||||
return {
|
return {
|
||||||
powerRatio: ratio,
|
powerRatio: ratio,
|
||||||
checkInterval: 240, // 4分钟
|
checkInterval: 240, // 4分钟
|
||||||
resourceGrowthRate: 0.6,
|
resourceGrowthRate: 1.0, // 从0.6提升到1.0,与玩家资源产出相当
|
||||||
buildingGrowthSpeed: 0.6,
|
buildingGrowthSpeed: 0.8, // 从0.6提升到0.8
|
||||||
techGrowthSpeed: 0.6
|
techGrowthSpeed: 0.8 // 从0.6提升到0.8
|
||||||
}
|
}
|
||||||
} else if (playerPoints < 20000) {
|
} else if (playerPoints < 20000) {
|
||||||
// 中级期:5,000-20,000分
|
// 中级期:5,000-20,000分
|
||||||
@@ -384,15 +384,16 @@ export const autoBuildNPCFleet = (npc: NPC): void => {
|
|||||||
|
|
||||||
if (!canBuild) continue
|
if (!canBuild) continue
|
||||||
|
|
||||||
// 根据难度和当前资源决定建造数量
|
// 根据难度和当前资源决定建造数量(防止除零)
|
||||||
const maxAffordable = Math.floor(
|
const metalAffordable = shipConfig.cost.metal > 0 ? planet.resources.metal / shipConfig.cost.metal : Infinity
|
||||||
Math.min(
|
const crystalAffordable = shipConfig.cost.crystal > 0 ? planet.resources.crystal / shipConfig.cost.crystal : Infinity
|
||||||
planet.resources.metal / shipConfig.cost.metal,
|
const deuteriumAffordable = shipConfig.cost.deuterium > 0 ? planet.resources.deuterium / shipConfig.cost.deuterium : Infinity
|
||||||
planet.resources.crystal / shipConfig.cost.crystal,
|
const darkMatterAffordable = shipConfig.cost.darkMatter > 0 ? planet.resources.darkMatter / shipConfig.cost.darkMatter : Infinity
|
||||||
planet.resources.deuterium / shipConfig.cost.deuterium,
|
|
||||||
shipConfig.cost.darkMatter > 0 ? planet.resources.darkMatter / shipConfig.cost.darkMatter : Infinity
|
const maxAffordable = Math.floor(Math.min(metalAffordable, crystalAffordable, deuteriumAffordable, darkMatterAffordable))
|
||||||
)
|
|
||||||
)
|
// 防止NaN或Infinity(如果所有成本都为0的极端情况)
|
||||||
|
if (!Number.isFinite(maxAffordable) || maxAffordable <= 0) continue
|
||||||
|
|
||||||
// 建造数量:简单1-5艘,中等5-10艘,困难10-20艘
|
// 建造数量:简单1-5艘,中等5-10艘,困难10-20艘
|
||||||
const buildCount = Math.min(maxAffordable, npc.difficulty === 'easy' ? 5 : npc.difficulty === 'medium' ? 10 : 20)
|
const buildCount = Math.min(maxAffordable, npc.difficulty === 'easy' ? 5 : npc.difficulty === 'medium' ? 10 : 20)
|
||||||
@@ -416,7 +417,7 @@ export const autoBuildNPCFleet = (npc: NPC): void => {
|
|||||||
/**
|
/**
|
||||||
* 为NPC生成资源(模拟资源生产)
|
* 为NPC生成资源(模拟资源生产)
|
||||||
*/
|
*/
|
||||||
export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: DynamicDifficultyConfig): void => {
|
export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: DynamicDifficultyConfig, gameSpeed: number = 1): void => {
|
||||||
const planet = npc.planets[0]
|
const planet = npc.planets[0]
|
||||||
if (!planet) return
|
if (!planet) return
|
||||||
|
|
||||||
@@ -432,11 +433,14 @@ export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: Dyn
|
|||||||
const deuteriumProduction = 10 * deuteriumLevel * Math.pow(1.1, deuteriumLevel) * 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 darkMatterProduction = ((25 * darkMatterLevel * Math.pow(1.5, darkMatterLevel)) / 3600) * config.resourceGrowthRate
|
||||||
|
|
||||||
|
// 应用游戏速度倍率到时间
|
||||||
|
const effectiveDeltaSeconds = deltaSeconds * gameSpeed
|
||||||
|
|
||||||
// 增加资源
|
// 增加资源
|
||||||
planet.resources.metal += metalProduction * deltaSeconds
|
planet.resources.metal += metalProduction * effectiveDeltaSeconds
|
||||||
planet.resources.crystal += crystalProduction * deltaSeconds
|
planet.resources.crystal += crystalProduction * effectiveDeltaSeconds
|
||||||
planet.resources.deuterium += deuteriumProduction * deltaSeconds
|
planet.resources.deuterium += deuteriumProduction * effectiveDeltaSeconds
|
||||||
planet.resources.darkMatter += darkMatterProduction * deltaSeconds
|
planet.resources.darkMatter += darkMatterProduction * effectiveDeltaSeconds
|
||||||
|
|
||||||
// 确保不超过存储上限
|
// 确保不超过存储上限
|
||||||
const metalStorage = planet.buildings[BuildingType.MetalStorage] || 0
|
const metalStorage = planet.buildings[BuildingType.MetalStorage] || 0
|
||||||
@@ -454,12 +458,12 @@ export const generateNPCResources = (npc: NPC, deltaSeconds: number, config: Dyn
|
|||||||
* 主NPC成长更新函数
|
* 主NPC成长更新函数
|
||||||
* 应该在游戏循环中定期调用
|
* 应该在游戏循环中定期调用
|
||||||
*/
|
*/
|
||||||
export const updateNPCGrowth = (npc: NPC, gameState: NPCGrowthGameState, deltaSeconds: number): void => {
|
export const updateNPCGrowth = (npc: NPC, gameState: NPCGrowthGameState, deltaSeconds: number, gameSpeed: number = 1): void => {
|
||||||
// 使用动态难度(基于玩家积分)而不是固定难度
|
// 使用动态难度(基于玩家积分)而不是固定难度
|
||||||
const config = calculateDynamicDifficulty(gameState.player.points)
|
const config = calculateDynamicDifficulty(gameState.player.points)
|
||||||
|
|
||||||
// 1. 持续生成资源
|
// 1. 持续生成资源(应用游戏速度倍率)
|
||||||
generateNPCResources(npc, deltaSeconds, config)
|
generateNPCResources(npc, deltaSeconds, config, gameSpeed)
|
||||||
|
|
||||||
// 2. 定期评估并调整实力(使用静态计数器或时间戳)
|
// 2. 定期评估并调整实力(使用静态计数器或时间戳)
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -542,6 +546,50 @@ export const initializeNPCStartingPower = (
|
|||||||
planet.resources.crystal = 50000 * config.powerRatio
|
planet.resources.crystal = 50000 * config.powerRatio
|
||||||
planet.resources.deuterium = 20000 * config.powerRatio
|
planet.resources.deuterium = 20000 * config.powerRatio
|
||||||
planet.resources.darkMatter = 1000 * config.powerRatio
|
planet.resources.darkMatter = 1000 * config.powerRatio
|
||||||
|
|
||||||
|
// 给予起始舰队(确保NPC能够立即侦查和攻击)
|
||||||
|
// 使用平方根函数来平滑舰队数量增长,避免初期过于强大
|
||||||
|
const fleetRatio = Math.sqrt(config.powerRatio)
|
||||||
|
|
||||||
|
// 间谍探测器 - 必需,用于侦查玩家
|
||||||
|
planet.fleet[ShipType.EspionageProbe] = Math.max(5, Math.floor(10 * fleetRatio))
|
||||||
|
|
||||||
|
// 基础战斗舰队
|
||||||
|
planet.fleet[ShipType.LightFighter] = Math.floor(20 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.HeavyFighter] = Math.floor(10 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.Cruiser] = Math.floor(5 * fleetRatio)
|
||||||
|
|
||||||
|
// 运输和回收舰船
|
||||||
|
planet.fleet[ShipType.SmallCargo] = Math.floor(5 * fleetRatio)
|
||||||
|
planet.fleet[ShipType.Recycler] = Math.floor(3 * fleetRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保所有NPC都有最低数量的间谍探测器
|
||||||
|
* 用于修复旧版本保存的NPC数据
|
||||||
|
*/
|
||||||
|
export const ensureNPCSpyProbes = (npcs: NPC[]): void => {
|
||||||
|
npcs.forEach(npc => {
|
||||||
|
const planet = npc.planets[0]
|
||||||
|
if (!planet) return
|
||||||
|
|
||||||
|
// 如果没有舰队数据,初始化
|
||||||
|
if (!planet.fleet) {
|
||||||
|
planet.fleet = {} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查间谍探测器数量
|
||||||
|
const currentProbes = planet.fleet[ShipType.EspionageProbe] || 0
|
||||||
|
|
||||||
|
// 如果没有探测器,根据NPC难度给予基础数量
|
||||||
|
if (currentProbes === 0) {
|
||||||
|
const config = NPC_GROWTH_CONFIG[npc.difficulty]
|
||||||
|
const fleetRatio = Math.sqrt(config.powerRatio)
|
||||||
|
planet.fleet[ShipType.EspionageProbe] = Math.max(5, Math.floor(10 * fleetRatio))
|
||||||
|
|
||||||
|
console.log(`[NPC Migration] Added ${planet.fleet[ShipType.EspionageProbe]} spy probes to NPC ${npc.name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -559,9 +607,7 @@ export const initializeNPCDiplomacy = (npcs: NPC[]): void => {
|
|||||||
// 为每个NPC随机分配1-2个盟友
|
// 为每个NPC随机分配1-2个盟友
|
||||||
npcs.forEach(npc => {
|
npcs.forEach(npc => {
|
||||||
// 获取还未建立关系的潜在盟友
|
// 获取还未建立关系的潜在盟友
|
||||||
const potentialAllies = npcs.filter(
|
const potentialAllies = npcs.filter(n => n.id !== npc.id && !npc.allies!.includes(n.id) && !n.allies!.includes(npc.id))
|
||||||
n => n.id !== npc.id && !npc.allies!.includes(n.id) && !n.allies!.includes(npc.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (potentialAllies.length === 0) return
|
if (potentialAllies.length === 0) return
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
* 提供跨模块共享的通用业务逻辑功能
|
* 提供跨模块共享的通用业务逻辑功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BuildingType, TechnologyType } from '@/types/game'
|
import { BuildingType, TechnologyType, ShipType, DefenseType } from '@/types/game'
|
||||||
import type { Planet, Resources, Officer, BuildingConfig, TechnologyConfig } from '@/types/game'
|
import type { Planet, Resources, Officer, BuildingConfig, TechnologyConfig, Player } from '@/types/game'
|
||||||
import { OfficerType } from '@/types/game'
|
import { OfficerType } from '@/types/game'
|
||||||
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 { scaleResources } from '@/utils/speed'
|
||||||
|
import { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES } from '@/config/gameConfig'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取特定等级的升级条件
|
* 获取特定等级的升级条件
|
||||||
@@ -93,11 +95,12 @@ export const checkRequirements = (
|
|||||||
* @param officers 玩家的军官对象
|
* @param officers 玩家的军官对象
|
||||||
* @returns 每小时各类资源的产量
|
* @returns 每小时各类资源的产量
|
||||||
*/
|
*/
|
||||||
export const getResourceProduction = (planet: Planet, officers: Record<OfficerType, Officer>): Resources => {
|
export const getResourceProduction = (planet: Planet, officers: Record<OfficerType, Officer>, resourceSpeed: number = 1): Resources => {
|
||||||
// 计算当前激活的军官加成
|
// 计算当前激活的军官加成
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||||
// 根据建筑等级和军官加成计算资源产量
|
// 根据建筑等级和军官加成计算资源产量
|
||||||
return resourceLogic.calculateResourceProduction(planet, bonuses)
|
const base = resourceLogic.calculateResourceProduction(planet, bonuses)
|
||||||
|
return scaleResources(base, resourceSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,8 +140,128 @@ export const getMaxResearchQueue = (technologies: Partial<Record<TechnologyType,
|
|||||||
/**
|
/**
|
||||||
* 计算最大舰队任务数量
|
* 计算最大舰队任务数量
|
||||||
* @param additionalFleetSlots 军官提供的额外槽位数量
|
* @param additionalFleetSlots 军官提供的额外槽位数量
|
||||||
* @returns 最大舰队任务数量(基础1个 + 军官加成,最多10个)
|
* @param computerTechnologyLevel 计算机技术等级
|
||||||
|
* @returns 最大舰队任务数量(基础1个 + 计算机技术等级 + 军官加成,最多20个)
|
||||||
*/
|
*/
|
||||||
export const getMaxFleetMissions = (additionalFleetSlots: number = 0): number => {
|
export const getMaxFleetMissions = (additionalFleetSlots: number = 0, computerTechnologyLevel: number = 0): number => {
|
||||||
return Math.min(1 + additionalFleetSlots, 10)
|
return Math.min(1 + computerTechnologyLevel + additionalFleetSlots, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算建筑的总成本(从等级1到目标等级的累计成本)
|
||||||
|
* @param buildingType 建筑类型
|
||||||
|
* @param level 目标等级
|
||||||
|
* @returns 总资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateBuildingTotalCost = (buildingType: BuildingType, level: number): number => {
|
||||||
|
if (level <= 0) return 0
|
||||||
|
|
||||||
|
const config = BUILDINGS[buildingType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
let totalCost = 0
|
||||||
|
const { baseCost, costMultiplier } = config
|
||||||
|
|
||||||
|
// 累加从等级1到目标等级的所有成本
|
||||||
|
for (let i = 1; i <= level; i++) {
|
||||||
|
const levelCost = {
|
||||||
|
metal: Math.floor(baseCost.metal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
crystal: Math.floor(baseCost.crystal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
deuterium: Math.floor(baseCost.deuterium * Math.pow(costMultiplier, i - 1))
|
||||||
|
}
|
||||||
|
totalCost += levelCost.metal + levelCost.crystal + levelCost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCost
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算科技的总成本(从等级1到目标等级的累计成本)
|
||||||
|
* @param techType 科技类型
|
||||||
|
* @param level 目标等级
|
||||||
|
* @returns 总资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateTechnologyTotalCost = (techType: TechnologyType, level: number): number => {
|
||||||
|
if (level <= 0) return 0
|
||||||
|
|
||||||
|
const config = TECHNOLOGIES[techType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
let totalCost = 0
|
||||||
|
const { baseCost, costMultiplier } = config
|
||||||
|
|
||||||
|
// 累加从等级1到目标等级的所有成本
|
||||||
|
for (let i = 1; i <= level; i++) {
|
||||||
|
const levelCost = {
|
||||||
|
metal: Math.floor(baseCost.metal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
crystal: Math.floor(baseCost.crystal * Math.pow(costMultiplier, i - 1)),
|
||||||
|
deuterium: Math.floor(baseCost.deuterium * Math.pow(costMultiplier, i - 1))
|
||||||
|
}
|
||||||
|
totalCost += levelCost.metal + levelCost.crystal + levelCost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCost
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个舰船的成本
|
||||||
|
* @param shipType 舰船类型
|
||||||
|
* @returns 单个舰船的资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateShipUnitCost = (shipType: ShipType): number => {
|
||||||
|
const config = SHIPS[shipType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
return config.cost.metal + config.cost.crystal + config.cost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个防御的成本
|
||||||
|
* @param defenseType 防御类型
|
||||||
|
* @returns 单个防御的资源成本(金属+水晶+重氢)
|
||||||
|
*/
|
||||||
|
const calculateDefenseUnitCost = (defenseType: DefenseType): number => {
|
||||||
|
const config = DEFENSES[defenseType]
|
||||||
|
if (!config) return 0
|
||||||
|
|
||||||
|
return config.cost.metal + config.cost.crystal + config.cost.deuterium
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算玩家的总积分
|
||||||
|
* 积分规则:(建筑成本 + 科技成本 + 舰队成本 + 防御成本) / 1000
|
||||||
|
* @param player 玩家对象
|
||||||
|
* @returns 玩家总积分
|
||||||
|
*/
|
||||||
|
export const calculatePlayerPoints = (player: Player): number => {
|
||||||
|
let totalCost = 0
|
||||||
|
|
||||||
|
// 1. 计算所有星球的建筑成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.buildings).forEach(([buildingType, level]) => {
|
||||||
|
totalCost += calculateBuildingTotalCost(buildingType as BuildingType, level)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 计算科技成本
|
||||||
|
Object.entries(player.technologies).forEach(([techType, level]) => {
|
||||||
|
totalCost += calculateTechnologyTotalCost(techType as TechnologyType, level)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 计算所有星球的舰队成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.fleet).forEach(([shipType, count]) => {
|
||||||
|
totalCost += calculateShipUnitCost(shipType as ShipType) * count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 计算所有星球的防御成本
|
||||||
|
player.planets.forEach(planet => {
|
||||||
|
Object.entries(planet.defense).forEach(([defenseType, count]) => {
|
||||||
|
totalCost += calculateDefenseUnitCost(defenseType as DefenseType) * count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 每1000资源 = 1积分
|
||||||
|
return Math.floor(totalCost / 1000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ export const completeResearchQueue = (
|
|||||||
researchQueue: BuildQueueItem[],
|
researchQueue: BuildQueueItem[],
|
||||||
technologies: Partial<Record<TechnologyType, number>>,
|
technologies: Partial<Record<TechnologyType, number>>,
|
||||||
now: number,
|
now: number,
|
||||||
onPointsEarned?: (points: number, type: 'technology', itemType: string, level: number) => void
|
onPointsEarned?: (points: number, type: 'technology', itemType: string, level: number) => void,
|
||||||
|
onCompleted?: (type: 'technology', itemType: string, level: number) => void
|
||||||
): BuildQueueItem[] => {
|
): BuildQueueItem[] => {
|
||||||
return researchQueue.filter(item => {
|
return researchQueue.filter(item => {
|
||||||
if (now >= item.endTime) {
|
if (now >= item.endTime) {
|
||||||
@@ -112,6 +113,12 @@ export const completeResearchQueue = (
|
|||||||
const points = pointsLogic.calculateTechnologyPoints(item.itemType as TechnologyType, oldLevel, newLevel)
|
const points = pointsLogic.calculateTechnologyPoints(item.itemType as TechnologyType, oldLevel, newLevel)
|
||||||
onPointsEarned(points, 'technology', item.itemType, newLevel)
|
onPointsEarned(points, 'technology', item.itemType, newLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知完成
|
||||||
|
if (onCompleted) {
|
||||||
|
onCompleted('technology', item.itemType, newLevel)
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ export const validateTechnologyResearch = (
|
|||||||
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
|
const cost = researchLogic.calculateTechnologyCost(techType, targetLevel)
|
||||||
|
|
||||||
// 检查队列中是否已存在该科技的研究任务
|
// 检查队列中是否已存在该科技的研究任务
|
||||||
const existingQueueItem = researchQueue.find(
|
const existingQueueItem = researchQueue.find(item => item.type === 'technology' && item.itemType === techType)
|
||||||
item => item.type === 'technology' && item.itemType === techType
|
|
||||||
)
|
|
||||||
if (existingQueueItem) {
|
if (existingQueueItem) {
|
||||||
return { valid: false, reason: 'errors.technologyAlreadyInQueue' }
|
return { valid: false, reason: 'errors.technologyAlreadyInQueue' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,16 +67,20 @@ export const calculateResourceProduction = (
|
|||||||
// 计算能量产出(每小时)
|
// 计算能量产出(每小时)
|
||||||
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
|
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
|
||||||
|
|
||||||
// 检查当前能量是否充足
|
// 计算能量消耗(每小时)
|
||||||
// 如果当前能量 <= 0,矿场停止生产
|
const energyConsumption = calculateEnergyConsumption(planet)
|
||||||
const hasEnergy = planet.resources.energy > 0
|
|
||||||
const productionEfficiency = hasEnergy ? 1 : 0
|
// 检查能量平衡是否充足
|
||||||
|
// 如果能量产出 >= 能量消耗,矿场正常生产
|
||||||
|
// 这样即使浏览器关闭后再打开,只要能量平衡是正的,就能正常生产
|
||||||
|
const hasPositiveEnergyBalance = energyProduction >= energyConsumption
|
||||||
|
const productionEfficiency = hasPositiveEnergyBalance ? 1 : 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
metal: metalMineLevel * 1500 * Math.pow(1.5, metalMineLevel) * resourceBonus * productionEfficiency,
|
||||||
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
|
crystal: crystalMineLevel * 1000 * Math.pow(1.5, crystalMineLevel) * resourceBonus * productionEfficiency,
|
||||||
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
|
deuterium: deuteriumSynthesizerLevel * 500 * Math.pow(1.5, deuteriumSynthesizerLevel) * resourceBonus * productionEfficiency,
|
||||||
darkMatter: darkMatterCollectorLevel * 25 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
|
darkMatter: darkMatterCollectorLevel * 100 * Math.pow(1.5, darkMatterCollectorLevel) * darkMatterBonus,
|
||||||
energy: energyProduction
|
energy: energyProduction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,23 +119,27 @@ export const updatePlanetResources = (
|
|||||||
darkMatterProductionBonus: number
|
darkMatterProductionBonus: number
|
||||||
energyProductionBonus: number
|
energyProductionBonus: number
|
||||||
storageCapacityBonus: number
|
storageCapacityBonus: number
|
||||||
}
|
},
|
||||||
|
gameSpeed: number = 1
|
||||||
): void => {
|
): void => {
|
||||||
const timeDiff = (now - planet.lastUpdate) / 1000 // 转换为秒
|
const timeDiff = (now - planet.lastUpdate) / 1000 // 转换为秒
|
||||||
|
|
||||||
|
// 应用游戏速度到时间差(游戏速度影响资源产出速率)
|
||||||
|
const effectiveTimeDiff = timeDiff * gameSpeed
|
||||||
|
|
||||||
// 计算能量消耗(每小时)
|
// 计算能量消耗(每小时)
|
||||||
const energyConsumption = calculateEnergyConsumption(planet)
|
const energyConsumption = calculateEnergyConsumption(planet)
|
||||||
|
|
||||||
// 先增加能量产出
|
// 先增加能量产出
|
||||||
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
|
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: bonuses.energyProductionBonus })
|
||||||
planet.resources.energy += (energyProduction * timeDiff) / 3600
|
planet.resources.energy += (energyProduction * effectiveTimeDiff) / 3600
|
||||||
|
|
||||||
// 限制能量上限
|
// 限制能量上限
|
||||||
const capacity = calculateResourceCapacity(planet, bonuses.storageCapacityBonus)
|
const capacity = calculateResourceCapacity(planet, bonuses.storageCapacityBonus)
|
||||||
planet.resources.energy = Math.min(planet.resources.energy, capacity.energy)
|
planet.resources.energy = Math.min(planet.resources.energy, capacity.energy)
|
||||||
|
|
||||||
// 扣除能量消耗
|
// 扣除能量消耗
|
||||||
planet.resources.energy -= (energyConsumption * timeDiff) / 3600
|
planet.resources.energy -= (energyConsumption * effectiveTimeDiff) / 3600
|
||||||
|
|
||||||
// 能量不能为负数,最低为0
|
// 能量不能为负数,最低为0
|
||||||
planet.resources.energy = Math.max(0, planet.resources.energy)
|
planet.resources.energy = Math.max(0, planet.resources.energy)
|
||||||
@@ -143,11 +151,11 @@ export const updatePlanetResources = (
|
|||||||
energyProductionBonus: bonuses.energyProductionBonus
|
energyProductionBonus: bonuses.energyProductionBonus
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新资源(转换为每秒产量)
|
// 更新资源(转换为每秒产量,应用游戏速度)
|
||||||
planet.resources.metal += (production.metal * timeDiff) / 3600
|
planet.resources.metal += (production.metal * effectiveTimeDiff) / 3600
|
||||||
planet.resources.crystal += (production.crystal * timeDiff) / 3600
|
planet.resources.crystal += (production.crystal * effectiveTimeDiff) / 3600
|
||||||
planet.resources.deuterium += (production.deuterium * timeDiff) / 3600
|
planet.resources.deuterium += (production.deuterium * effectiveTimeDiff) / 3600
|
||||||
planet.resources.darkMatter += (production.darkMatter * timeDiff) / 3600
|
planet.resources.darkMatter += (production.darkMatter * effectiveTimeDiff) / 3600
|
||||||
|
|
||||||
// 限制资源上限
|
// 限制资源上限
|
||||||
planet.resources.metal = Math.min(planet.resources.metal, capacity.metal)
|
planet.resources.metal = Math.min(planet.resources.metal, capacity.metal)
|
||||||
@@ -190,6 +198,44 @@ export const addResources = (currentResources: Resources, amount: Resources): vo
|
|||||||
currentResources.darkMatter += amount.darkMatter
|
currentResources.darkMatter += amount.darkMatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地添加资源(会检查仓储容量上限)
|
||||||
|
* @param planet 星球对象
|
||||||
|
* @param amount 要添加的资源
|
||||||
|
* @param storageCapacityBonus 仓储容量加成
|
||||||
|
* @returns 实际添加的资源数量和溢出的资源数量
|
||||||
|
*/
|
||||||
|
export const addResourcesSafely = (
|
||||||
|
planet: Planet,
|
||||||
|
amount: Resources,
|
||||||
|
storageCapacityBonus: number
|
||||||
|
): { added: Resources; overflow: Resources } => {
|
||||||
|
const capacity = calculateResourceCapacity(planet, storageCapacityBonus)
|
||||||
|
|
||||||
|
const added: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||||
|
const overflow: Resources = { metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 }
|
||||||
|
|
||||||
|
// 处理每种资源
|
||||||
|
const resources: Array<keyof Resources> = ['metal', 'crystal', 'deuterium', 'darkMatter']
|
||||||
|
|
||||||
|
for (const resourceType of resources) {
|
||||||
|
const currentAmount = planet.resources[resourceType]
|
||||||
|
const amountToAdd = amount[resourceType]
|
||||||
|
const maxCapacity = capacity[resourceType]
|
||||||
|
|
||||||
|
// 计算可以添加的量(不超过容量上限)
|
||||||
|
const spaceAvailable = Math.max(0, maxCapacity - currentAmount)
|
||||||
|
const actuallyAdded = Math.min(amountToAdd, spaceAvailable)
|
||||||
|
const overflowed = amountToAdd - actuallyAdded
|
||||||
|
|
||||||
|
planet.resources[resourceType] += actuallyAdded
|
||||||
|
added[resourceType] = actuallyAdded
|
||||||
|
overflow[resourceType] = overflowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return { added, overflow }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 资源产量详细信息(用于UI展示)
|
* 资源产量详细信息(用于UI展示)
|
||||||
*/
|
*/
|
||||||
@@ -245,7 +291,8 @@ export interface ConsumptionDetail {
|
|||||||
export const calculateProductionBreakdown = (
|
export const calculateProductionBreakdown = (
|
||||||
planet: Planet,
|
planet: Planet,
|
||||||
officers: Record<OfficerType, Officer>,
|
officers: Record<OfficerType, Officer>,
|
||||||
currentTime: number
|
currentTime: number,
|
||||||
|
resourceSpeed: number = 1
|
||||||
): ProductionBreakdown => {
|
): ProductionBreakdown => {
|
||||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||||
@@ -253,8 +300,11 @@ export const calculateProductionBreakdown = (
|
|||||||
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
const darkMatterCollectorLevel = planet.buildings[BuildingType.DarkMatterCollector] || 0
|
||||||
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
|
const solarPlantLevel = planet.buildings[BuildingType.SolarPlant] || 0
|
||||||
|
|
||||||
const hasEnergy = planet.resources.energy > 0
|
// 计算能量平衡(基于产出vs消耗,而不是当前能量值)
|
||||||
const productionEfficiency = hasEnergy ? 1 : 0
|
const energyProduction = calculateEnergyProduction(planet, { energyProductionBonus: 0 })
|
||||||
|
const energyConsumption = calculateEnergyConsumption(planet)
|
||||||
|
const hasPositiveEnergyBalance = energyProduction >= energyConsumption
|
||||||
|
const productionEfficiency = hasPositiveEnergyBalance ? 1 : 0
|
||||||
|
|
||||||
// 收集每个军官的加成信息
|
// 收集每个军官的加成信息
|
||||||
const activeOfficerBonuses: Array<{
|
const activeOfficerBonuses: Array<{
|
||||||
@@ -300,7 +350,7 @@ export const calculateProductionBreakdown = (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hasEnergy) {
|
if (!hasPositiveEnergyBalance) {
|
||||||
metalBonuses.push({
|
metalBonuses.push({
|
||||||
name: 'resources.noEnergy',
|
name: 'resources.noEnergy',
|
||||||
percentage: -100,
|
percentage: -100,
|
||||||
@@ -327,7 +377,7 @@ export const calculateProductionBreakdown = (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hasEnergy) {
|
if (!hasPositiveEnergyBalance) {
|
||||||
crystalBonuses.push({
|
crystalBonuses.push({
|
||||||
name: 'resources.noEnergy',
|
name: 'resources.noEnergy',
|
||||||
percentage: -100,
|
percentage: -100,
|
||||||
@@ -354,7 +404,7 @@ export const calculateProductionBreakdown = (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!hasEnergy) {
|
if (!hasPositiveEnergyBalance) {
|
||||||
deuteriumBonuses.push({
|
deuteriumBonuses.push({
|
||||||
name: 'resources.noEnergy',
|
name: 'resources.noEnergy',
|
||||||
percentage: -100,
|
percentage: -100,
|
||||||
@@ -431,42 +481,56 @@ export const calculateProductionBreakdown = (
|
|||||||
|
|
||||||
const energyFinal = energyBase * (1 + totalEnergyBonus / 100)
|
const energyFinal = energyBase * (1 + totalEnergyBonus / 100)
|
||||||
|
|
||||||
|
const speed = resourceSpeed
|
||||||
|
|
||||||
|
const scaleBonuses = (bonuses: ProductionBonus[]) =>
|
||||||
|
bonuses.map(bonus => ({
|
||||||
|
...bonus,
|
||||||
|
value: bonus.value * speed
|
||||||
|
}))
|
||||||
|
|
||||||
|
const scaleSources = (sources?: ProductionSource[]) =>
|
||||||
|
sources?.map(source => ({
|
||||||
|
...source,
|
||||||
|
production: source.production * speed
|
||||||
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metal: {
|
metal: {
|
||||||
baseProduction: metalBase,
|
baseProduction: metalBase * speed,
|
||||||
buildingLevel: metalMineLevel,
|
buildingLevel: metalMineLevel,
|
||||||
buildingName: 'buildings.metalMine',
|
buildingName: 'buildings.metalMine',
|
||||||
bonuses: metalBonuses,
|
bonuses: scaleBonuses(metalBonuses),
|
||||||
finalProduction: metalFinal
|
finalProduction: metalFinal * speed
|
||||||
},
|
},
|
||||||
crystal: {
|
crystal: {
|
||||||
baseProduction: crystalBase,
|
baseProduction: crystalBase * speed,
|
||||||
buildingLevel: crystalMineLevel,
|
buildingLevel: crystalMineLevel,
|
||||||
buildingName: 'buildings.crystalMine',
|
buildingName: 'buildings.crystalMine',
|
||||||
bonuses: crystalBonuses,
|
bonuses: scaleBonuses(crystalBonuses),
|
||||||
finalProduction: crystalFinal
|
finalProduction: crystalFinal * speed
|
||||||
},
|
},
|
||||||
deuterium: {
|
deuterium: {
|
||||||
baseProduction: deuteriumBase,
|
baseProduction: deuteriumBase * speed,
|
||||||
buildingLevel: deuteriumSynthesizerLevel,
|
buildingLevel: deuteriumSynthesizerLevel,
|
||||||
buildingName: 'buildings.deuteriumSynthesizer',
|
buildingName: 'buildings.deuteriumSynthesizer',
|
||||||
bonuses: deuteriumBonuses,
|
bonuses: scaleBonuses(deuteriumBonuses),
|
||||||
finalProduction: deuteriumFinal
|
finalProduction: deuteriumFinal * speed
|
||||||
},
|
},
|
||||||
darkMatter: {
|
darkMatter: {
|
||||||
baseProduction: darkMatterBase,
|
baseProduction: darkMatterBase * speed,
|
||||||
buildingLevel: darkMatterCollectorLevel,
|
buildingLevel: darkMatterCollectorLevel,
|
||||||
buildingName: 'buildings.darkMatterCollector',
|
buildingName: 'buildings.darkMatterCollector',
|
||||||
bonuses: darkMatterBonuses,
|
bonuses: scaleBonuses(darkMatterBonuses),
|
||||||
finalProduction: darkMatterFinal
|
finalProduction: darkMatterFinal * speed
|
||||||
},
|
},
|
||||||
energy: {
|
energy: {
|
||||||
baseProduction: energyBase,
|
baseProduction: energyBase * speed,
|
||||||
buildingLevel: solarPlantLevel,
|
buildingLevel: solarPlantLevel,
|
||||||
buildingName: 'buildings.solarPlant',
|
buildingName: 'buildings.solarPlant',
|
||||||
bonuses: energyBonuses,
|
bonuses: scaleBonuses(energyBonuses),
|
||||||
finalProduction: energyFinal,
|
finalProduction: energyFinal * speed,
|
||||||
sources: energySources
|
sources: scaleSources(energySources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,7 +538,7 @@ export const calculateProductionBreakdown = (
|
|||||||
/**
|
/**
|
||||||
* 计算能量消耗详细breakdown
|
* 计算能量消耗详细breakdown
|
||||||
*/
|
*/
|
||||||
export const calculateConsumptionBreakdown = (planet: Planet): ConsumptionBreakdown => {
|
export const calculateConsumptionBreakdown = (planet: Planet, resourceSpeed: number = 1): ConsumptionBreakdown => {
|
||||||
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
const metalMineLevel = planet.buildings[BuildingType.MetalMine] || 0
|
||||||
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
const crystalMineLevel = planet.buildings[BuildingType.CrystalMine] || 0
|
||||||
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
|
const deuteriumSynthesizerLevel = planet.buildings[BuildingType.DeuteriumSynthesizer] || 0
|
||||||
@@ -483,22 +547,24 @@ export const calculateConsumptionBreakdown = (planet: Planet): ConsumptionBreakd
|
|||||||
const crystalConsumption = crystalMineLevel * 10 * Math.pow(1.1, crystalMineLevel)
|
const crystalConsumption = crystalMineLevel * 10 * Math.pow(1.1, crystalMineLevel)
|
||||||
const deuteriumConsumption = deuteriumSynthesizerLevel * 15 * Math.pow(1.1, deuteriumSynthesizerLevel)
|
const deuteriumConsumption = deuteriumSynthesizerLevel * 15 * Math.pow(1.1, deuteriumSynthesizerLevel)
|
||||||
|
|
||||||
|
const speed = resourceSpeed
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metalMine: {
|
metalMine: {
|
||||||
buildingLevel: metalMineLevel,
|
buildingLevel: metalMineLevel,
|
||||||
buildingName: 'buildings.metalMine',
|
buildingName: 'buildings.metalMine',
|
||||||
consumption: metalConsumption
|
consumption: metalConsumption * speed
|
||||||
},
|
},
|
||||||
crystalMine: {
|
crystalMine: {
|
||||||
buildingLevel: crystalMineLevel,
|
buildingLevel: crystalMineLevel,
|
||||||
buildingName: 'buildings.crystalMine',
|
buildingName: 'buildings.crystalMine',
|
||||||
consumption: crystalConsumption
|
consumption: crystalConsumption * speed
|
||||||
},
|
},
|
||||||
deuteriumSynthesizer: {
|
deuteriumSynthesizer: {
|
||||||
buildingLevel: deuteriumSynthesizerLevel,
|
buildingLevel: deuteriumSynthesizerLevel,
|
||||||
buildingName: 'buildings.deuteriumSynthesizer',
|
buildingName: 'buildings.deuteriumSynthesizer',
|
||||||
consumption: deuteriumConsumption
|
consumption: deuteriumConsumption * speed
|
||||||
},
|
},
|
||||||
total: metalConsumption + crystalConsumption + deuteriumConsumption
|
total: (metalConsumption + crystalConsumption + deuteriumConsumption) * speed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,24 @@ export const calculateCurrentMissileCount = (defense: Partial<Record<DefenseType
|
|||||||
return interplanetaryMissiles + antiBallisticMissiles
|
return interplanetaryMissiles + antiBallisticMissiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算建造队列中的导弹总数
|
||||||
|
*/
|
||||||
|
export const calculateQueueMissileCount = (buildQueue: Array<{ type: string; itemType: string; quantity?: number }>): number => {
|
||||||
|
let queueMissileCount = 0
|
||||||
|
|
||||||
|
for (const item of buildQueue) {
|
||||||
|
if (item.type === 'defense') {
|
||||||
|
const defenseType = item.itemType as DefenseType
|
||||||
|
if (defenseType === DefenseType.InterplanetaryMissile || defenseType === DefenseType.AntiBallisticMissile) {
|
||||||
|
queueMissileCount += item.quantity || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queueMissileCount
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查导弹容量限制
|
* 检查导弹容量限制
|
||||||
*/
|
*/
|
||||||
@@ -180,7 +198,8 @@ export const checkMissileSiloLimit = (
|
|||||||
defenseType: DefenseType,
|
defenseType: DefenseType,
|
||||||
currentDefense: Partial<Record<DefenseType, number>>,
|
currentDefense: Partial<Record<DefenseType, number>>,
|
||||||
buildings: Partial<Record<BuildingType, number>>,
|
buildings: Partial<Record<BuildingType, number>>,
|
||||||
quantity: number
|
quantity: number,
|
||||||
|
buildQueue?: Array<{ type: string; itemType: string; quantity?: number }>
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// 只对导弹类型进行检查
|
// 只对导弹类型进行检查
|
||||||
if (defenseType !== DefenseType.InterplanetaryMissile && defenseType !== DefenseType.AntiBallisticMissile) {
|
if (defenseType !== DefenseType.InterplanetaryMissile && defenseType !== DefenseType.AntiBallisticMissile) {
|
||||||
@@ -189,7 +208,8 @@ export const checkMissileSiloLimit = (
|
|||||||
|
|
||||||
const maxCapacity = calculateMissileSiloCapacity(buildings)
|
const maxCapacity = calculateMissileSiloCapacity(buildings)
|
||||||
const currentCount = calculateCurrentMissileCount(currentDefense)
|
const currentCount = calculateCurrentMissileCount(currentDefense)
|
||||||
const newCount = currentCount + quantity
|
const queueCount = buildQueue ? calculateQueueMissileCount(buildQueue) : 0
|
||||||
|
const newCount = currentCount + queueCount + quantity
|
||||||
|
|
||||||
return newCount <= maxCapacity
|
return newCount <= maxCapacity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const validateDefenseBuild = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导弹发射井容量限制
|
// 导弹发射井容量限制
|
||||||
if (!shipLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity)) {
|
if (!shipLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity, planet.buildQueue)) {
|
||||||
return { valid: false, reason: 'errors.missileSiloLimit' }
|
return { valid: false, reason: 'errors.missileSiloLimit' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,8 @@ export const validateFleetDispatch = (
|
|||||||
fleet: Partial<Fleet>,
|
fleet: Partial<Fleet>,
|
||||||
cargo: Resources,
|
cargo: Resources,
|
||||||
officers: Record<OfficerType, Officer>,
|
officers: Record<OfficerType, Officer>,
|
||||||
currentFleetMissions: number = 0
|
currentFleetMissions: number = 0,
|
||||||
|
technologies: Partial<Record<TechnologyType, number>> = {}
|
||||||
): {
|
): {
|
||||||
valid: boolean
|
valid: boolean
|
||||||
reason?: string
|
reason?: string
|
||||||
@@ -159,7 +160,8 @@ export const validateFleetDispatch = (
|
|||||||
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(officers, Date.now())
|
||||||
|
|
||||||
// 检查舰队任务槽位是否已满
|
// 检查舰队任务槽位是否已满
|
||||||
const maxFleetMissions = publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
const computerTechLevel = technologies[TechnologyType.ComputerTechnology] || 0
|
||||||
|
const maxFleetMissions = publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||||
if (currentFleetMissions >= maxFleetMissions) {
|
if (currentFleetMissions >= maxFleetMissions) {
|
||||||
return { valid: false, reason: 'errors.fleetMissionsFull' }
|
return { valid: false, reason: 'errors.fleetMissionsFull' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'overview', component: () => import('@/views/OverviewView.vue') },
|
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
|
||||||
|
{ path: '/overview', name: 'overview', component: () => import('@/views/OverviewView.vue') },
|
||||||
{ path: '/buildings', name: 'buildings', component: () => import('@/views/BuildingsView.vue') },
|
{ path: '/buildings', name: 'buildings', component: () => import('@/views/BuildingsView.vue') },
|
||||||
{ path: '/research', name: 'research', component: () => import('@/views/ResearchView.vue') },
|
{ path: '/research', name: 'research', component: () => import('@/views/ResearchView.vue') },
|
||||||
{ path: '/shipyard', name: 'shipyard', component: () => import('@/views/ShipyardView.vue') },
|
{ path: '/shipyard', name: 'shipyard', component: () => import('@/views/ShipyardView.vue') },
|
||||||
@@ -20,4 +22,31 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 路由守卫:检查隐私协议同意状态
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
|
// 已同意隐私协议
|
||||||
|
if (gameStore.player.privacyAgreed) {
|
||||||
|
// 已同意但访问首页,重定向到总览页
|
||||||
|
if (to.path === '/') {
|
||||||
|
next('/overview')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 正常访问其他页面
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未同意隐私协议
|
||||||
|
// 允许访问首页(用于显示隐私协议同意弹窗)
|
||||||
|
if (to.path === '/') {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未同意隐私协议且访问其他页面,重定向到首页
|
||||||
|
next('/')
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { Planet, Player, BuildQueueItem, FleetMission, BattleResult, SpyReport, Officer, SpiedNotification, NPCActivityNotification, IncomingFleetAlert, MissileAttack } from '@/types/game'
|
import type {
|
||||||
|
Planet,
|
||||||
|
Player,
|
||||||
|
BuildQueueItem,
|
||||||
|
FleetMission,
|
||||||
|
BattleResult,
|
||||||
|
SpyReport,
|
||||||
|
Officer,
|
||||||
|
SpiedNotification,
|
||||||
|
NPCActivityNotification,
|
||||||
|
IncomingFleetAlert,
|
||||||
|
MissileAttack
|
||||||
|
} from '@/types/game'
|
||||||
import { TechnologyType, OfficerType } from '@/types/game'
|
import { TechnologyType, OfficerType } from '@/types/game'
|
||||||
import type { Locale } from '@/locales'
|
import type { Locale } from '@/locales'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../package.json'
|
||||||
@@ -33,8 +45,27 @@ export const useGameStore = defineStore('game', {
|
|||||||
} as Player,
|
} as Player,
|
||||||
currentPlanetId: '',
|
currentPlanetId: '',
|
||||||
isDark: '',
|
isDark: '',
|
||||||
locale: 'zh-CN' as Locale
|
locale: 'zh-CN' as Locale,
|
||||||
|
notificationSettings: {
|
||||||
|
browser: false,
|
||||||
|
inApp: true,
|
||||||
|
suppressInFocus: false,
|
||||||
|
types: {
|
||||||
|
construction: true,
|
||||||
|
research: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
actions: {
|
||||||
|
async requestBrowserPermission(): Promise<boolean> {
|
||||||
|
if (!('Notification' in window)) return false
|
||||||
|
|
||||||
|
if (Notification.permission === 'granted') return true
|
||||||
|
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
return permission === 'granted'
|
||||||
|
}
|
||||||
|
},
|
||||||
getters: {
|
getters: {
|
||||||
currentPlanet(): Planet | undefined {
|
currentPlanet(): Planet | undefined {
|
||||||
return this.player.planets.find(p => p.id === this.currentPlanetId)
|
return this.player.planets.find(p => p.id === this.currentPlanetId)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "@/assets/main.css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const BuildingType = {
|
|||||||
RoboticsFactory: 'roboticsFactory',
|
RoboticsFactory: 'roboticsFactory',
|
||||||
NaniteFactory: 'naniteFactory', // 纳米工厂
|
NaniteFactory: 'naniteFactory', // 纳米工厂
|
||||||
Shipyard: 'shipyard',
|
Shipyard: 'shipyard',
|
||||||
|
Hangar: 'hangar', // 机库
|
||||||
ResearchLab: 'researchLab',
|
ResearchLab: 'researchLab',
|
||||||
MetalStorage: 'metalStorage',
|
MetalStorage: 'metalStorage',
|
||||||
CrystalStorage: 'crystalStorage',
|
CrystalStorage: 'crystalStorage',
|
||||||
@@ -227,7 +228,8 @@ export const DiplomaticEventType = {
|
|||||||
Attack: 'attack', // 攻击
|
Attack: 'attack', // 攻击
|
||||||
Spy: 'spy', // 侦查
|
Spy: 'spy', // 侦查
|
||||||
StealDebris: 'stealDebris', // 抢夺残骸
|
StealDebris: 'stealDebris', // 抢夺残骸
|
||||||
AllyAttacked: 'allyAttacked' // 盟友被攻击
|
AllyAttacked: 'allyAttacked', // 盟友被攻击
|
||||||
|
DestroyPlanet: 'destroyPlanet' // 摧毁星球
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type DiplomaticEventType = (typeof DiplomaticEventType)[keyof typeof DiplomaticEventType]
|
export type DiplomaticEventType = (typeof DiplomaticEventType)[keyof typeof DiplomaticEventType]
|
||||||
@@ -259,7 +261,9 @@ export interface DiplomaticReport {
|
|||||||
newReputation: number // 新的好感度值
|
newReputation: number // 新的好感度值
|
||||||
oldStatus: RelationStatus // 旧的关系状态
|
oldStatus: RelationStatus // 旧的关系状态
|
||||||
newStatus: RelationStatus // 新的关系状态
|
newStatus: RelationStatus // 新的关系状态
|
||||||
message: string // 消息内容
|
message: string // 消息内容(已弃用,保留用于兼容性)
|
||||||
|
messageKey?: string // 翻译键(如 'diplomacy.reports.youDestroyedNpcPlanet')
|
||||||
|
messageParams?: Record<string, string | number> // 翻译参数(如 { npcName: 'NPC-1', planetName: '星球 1:1:8', reputation: -80 })
|
||||||
read?: boolean // 已读状态
|
read?: boolean // 已读状态
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +430,12 @@ export interface MissionReport {
|
|||||||
missileHits?: number
|
missileHits?: number
|
||||||
missileIntercepted?: number
|
missileIntercepted?: number
|
||||||
defenseLosses?: Partial<Record<DefenseType, number>>
|
defenseLosses?: Partial<Record<DefenseType, number>>
|
||||||
|
// 探险任务:发现的资源
|
||||||
|
foundResources?: Partial<Resources>
|
||||||
|
// 探险任务:发现的舰船
|
||||||
|
foundFleet?: Partial<Fleet>
|
||||||
|
// 探险任务:损失的舰船
|
||||||
|
fleetLost?: Partial<Fleet>
|
||||||
}
|
}
|
||||||
read?: boolean
|
read?: boolean
|
||||||
}
|
}
|
||||||
@@ -565,9 +575,28 @@ export interface Player {
|
|||||||
isGMEnabled?: boolean // GM模式开关(默认false,通过秘籍激活)
|
isGMEnabled?: boolean // GM模式开关(默认false,通过秘籍激活)
|
||||||
lastVersionCheckTime?: number // 最后一次自动检查版本的时间戳(被动检测)
|
lastVersionCheckTime?: number // 最后一次自动检查版本的时间戳(被动检测)
|
||||||
lastManualUpdateCheck?: number // 最后一次手动检查更新的时间戳(主动检测)
|
lastManualUpdateCheck?: number // 最后一次手动检查更新的时间戳(主动检测)
|
||||||
// 外交系统字段
|
// 外交系统字段(外交关系存储在 NPC.relations 中)
|
||||||
diplomaticRelations?: Record<string, DiplomaticRelation> // 玩家对NPC的关系(key: npcId)
|
|
||||||
diplomaticReports?: DiplomaticReport[] // 外交变化报告
|
diplomaticReports?: DiplomaticReport[] // 外交变化报告
|
||||||
|
// 新手引导字段
|
||||||
|
tutorialProgress?: TutorialProgress // 新手引导进度
|
||||||
|
// 隐私协议同意状态
|
||||||
|
privacyAgreed?: boolean // 是否已同意隐私协议
|
||||||
|
// 弱引导系统
|
||||||
|
dismissedHints?: string[] // 已关闭的提示ID列表
|
||||||
|
hintsEnabled?: boolean // 是否启用弱引导提示(默认true)
|
||||||
|
// 显示设置
|
||||||
|
backgroundEnabled?: boolean // 是否启用背景动画(默认false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationSettings {
|
||||||
|
browser: boolean
|
||||||
|
inApp: boolean
|
||||||
|
suppressInFocus: boolean // 当页面聚焦时是否浏览器通知
|
||||||
|
types: {
|
||||||
|
construction: boolean
|
||||||
|
research: boolean
|
||||||
|
[key: string]: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游戏状态
|
// 游戏状态
|
||||||
@@ -592,6 +621,7 @@ export interface Universe {
|
|||||||
export interface NPC {
|
export interface NPC {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
note?: string // 玩家添加的备注
|
||||||
planets: Planet[]
|
planets: Planet[]
|
||||||
technologies: Record<TechnologyType, number>
|
technologies: Record<TechnologyType, number>
|
||||||
difficulty: 'easy' | 'medium' | 'hard'
|
difficulty: 'easy' | 'medium' | 'hard'
|
||||||
@@ -616,3 +646,33 @@ export interface NPC {
|
|||||||
allies?: string[] // 盟友列表(NPC ID)
|
allies?: string[] // 盟友列表(NPC ID)
|
||||||
enemies?: string[] // 敌人列表(NPC ID)
|
enemies?: string[] // 敌人列表(NPC ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新手引导系统
|
||||||
|
export interface TutorialStep {
|
||||||
|
id: string
|
||||||
|
title: string // 标题
|
||||||
|
content: string // 内容描述
|
||||||
|
target?: string // 目标元素的选择器或ID
|
||||||
|
placement?: 'top' | 'bottom' | 'left' | 'right' | 'center' // 提示框位置
|
||||||
|
route?: string // 需要跳转的路由
|
||||||
|
action?: 'click' | 'build' | 'research' | 'none' // 需要完成的操作类型
|
||||||
|
actionTarget?: string // 操作目标(建筑ID、科技ID等)
|
||||||
|
completionCheck?: () => boolean // 完成条件检查函数(运行时注入)
|
||||||
|
canSkip?: boolean // 是否可跳过此步骤
|
||||||
|
highlightPadding?: number // 高亮区域的padding
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TutorialState {
|
||||||
|
isActive: boolean // 引导是否激活
|
||||||
|
currentStepIndex: number // 当前步骤索引
|
||||||
|
completedSteps: string[] // 已完成的步骤ID列表
|
||||||
|
skipped: boolean // 是否已跳过整个引导
|
||||||
|
lastActiveTime?: number // 最后活跃时间
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TutorialProgress {
|
||||||
|
tutorialCompleted: boolean // 是否完成了整个引导
|
||||||
|
completedStepIds: string[] // 已完成的步骤ID
|
||||||
|
currentStep: string | null // 当前步骤ID
|
||||||
|
skippedAt?: number // 跳过的时间戳
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化数字为英文单位(K, M, B, T, Q)
|
* 格式化数字为英文单位(K, M, B, T, Q)
|
||||||
* @param num 数字
|
* @param num 数字
|
||||||
@@ -32,6 +31,30 @@ export const getResourceColor = (current: number, max: number): string => {
|
|||||||
if (ratio >= 0.7) return 'text-yellow-600 dark:text-yellow-400'
|
if (ratio >= 0.7) return 'text-yellow-600 dark:text-yellow-400'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 格式化相对时间(用于显示"多久之前")
|
||||||
|
* @param seconds 秒数
|
||||||
|
* @param t 翻译函数
|
||||||
|
* @returns 格式化后的相对时间字符串
|
||||||
|
*/
|
||||||
|
export const formatRelativeTime = (seconds: number, t: (key: string) => string): string => {
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}${t('time.days')}${hours}${t('time.hours')}`
|
||||||
|
}
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}${t('time.hours')}${minutes}${t('time.minutes')}`
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}${t('time.minutes')}`
|
||||||
|
}
|
||||||
|
return `${secs}${t('time.seconds')}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间(秒转为 年:天:时:分:秒)
|
* 格式化时间(秒转为 年:天:时:分:秒)
|
||||||
* @param seconds 秒数
|
* @param seconds 秒数
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Planet, DebrisField } from '@/types/game'
|
import type { Planet, DebrisField, NPC } from '@/types/game'
|
||||||
import { decryptData, encryptData } from './crypto'
|
import { decryptData, encryptData } from './crypto'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../package.json'
|
||||||
|
|
||||||
@@ -33,10 +33,116 @@ export const migrateGameData = (): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要迁移
|
// 标记是否有数据需要保存
|
||||||
const hasOldMapData = oldData.universePlanets || oldData.debrisFields
|
let needsSave = false
|
||||||
if (!hasOldMapData) return
|
|
||||||
|
|
||||||
|
// 修复NPC数据(确保所有必需字段都存在)
|
||||||
|
if (oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||||
|
const now = Date.now()
|
||||||
|
const playerId = oldData.player?.id
|
||||||
|
|
||||||
|
oldData.npcs.forEach((npc: NPC) => {
|
||||||
|
// 确保NPC有必需的时间字段,并设置随机冷却避免同时行动
|
||||||
|
if (npc.lastSpyTime === undefined || npc.lastSpyTime === 0) {
|
||||||
|
// 0-4分钟的随机延迟
|
||||||
|
const randomSpyOffset = Math.random() * 240 * 1000
|
||||||
|
npc.lastSpyTime = now - randomSpyOffset
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
if (npc.lastAttackTime === undefined || npc.lastAttackTime === 0) {
|
||||||
|
// 0-8分钟的随机延迟
|
||||||
|
const randomAttackOffset = Math.random() * 480 * 1000
|
||||||
|
npc.lastAttackTime = now - randomAttackOffset
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
// 确保NPC有必需的数组字段
|
||||||
|
if (!npc.fleetMissions) {
|
||||||
|
npc.fleetMissions = []
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
if (!npc.playerSpyReports) {
|
||||||
|
npc.playerSpyReports = {}
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
if (!npc.relations) {
|
||||||
|
npc.relations = {}
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
if (!npc.allies) {
|
||||||
|
npc.allies = []
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
if (!npc.enemies) {
|
||||||
|
npc.enemies = []
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果NPC与玩家没有建立关系,自动建立中立关系
|
||||||
|
if (playerId && !npc.relations[playerId]) {
|
||||||
|
npc.relations[playerId] = {
|
||||||
|
fromId: npc.id,
|
||||||
|
toId: playerId,
|
||||||
|
reputation: 0,
|
||||||
|
status: 'neutral' as const,
|
||||||
|
lastUpdated: now,
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化玩家积分(如果不存在)
|
||||||
|
if (oldData.player && oldData.player.points === undefined) {
|
||||||
|
// 积分会在游戏启动时通过 initGame 计算,这里设置为0
|
||||||
|
oldData.player.points = 0
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移 player.diplomaticRelations 到 npc.relations
|
||||||
|
// 旧版本使用 player.diplomaticRelations[npcId] 存储玩家对NPC的关系
|
||||||
|
// 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系
|
||||||
|
if (oldData.player?.diplomaticRelations && oldData.npcs && Array.isArray(oldData.npcs)) {
|
||||||
|
const playerId = oldData.player.id
|
||||||
|
const playerRelations = oldData.player.diplomaticRelations as Record<string, any>
|
||||||
|
|
||||||
|
Object.entries(playerRelations).forEach(([npcId, relation]) => {
|
||||||
|
const npc = oldData.npcs.find((n: NPC) => n.id === npcId)
|
||||||
|
if (npc) {
|
||||||
|
if (!npc.relations) {
|
||||||
|
npc.relations = {}
|
||||||
|
}
|
||||||
|
// 如果NPC对玩家的关系不存在,使用玩家对NPC的关系数据
|
||||||
|
if (!npc.relations[playerId]) {
|
||||||
|
npc.relations[playerId] = {
|
||||||
|
...relation,
|
||||||
|
fromId: npcId,
|
||||||
|
toId: playerId
|
||||||
|
}
|
||||||
|
needsSave = true
|
||||||
|
} else {
|
||||||
|
// 如果两边都有数据,使用声望值更极端的那个(偏离0更远的)
|
||||||
|
const existingReputation = npc.relations[playerId].reputation || 0
|
||||||
|
const playerReputation = relation.reputation || 0
|
||||||
|
if (Math.abs(playerReputation) > Math.abs(existingReputation)) {
|
||||||
|
npc.relations[playerId].reputation = playerReputation
|
||||||
|
npc.relations[playerId].status = relation.status
|
||||||
|
needsSave = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除旧的 diplomaticRelations 字段
|
||||||
|
delete oldData.player.diplomaticRelations
|
||||||
|
needsSave = true
|
||||||
|
console.log('[Migration] Migrated player.diplomaticRelations to npc.relations')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要迁移地图数据
|
||||||
|
const hasOldMapData = oldData.universePlanets || oldData.debrisFields
|
||||||
|
|
||||||
|
if (hasOldMapData) {
|
||||||
// 准备 universeStore 数据
|
// 准备 universeStore 数据
|
||||||
const universeData: {
|
const universeData: {
|
||||||
planets: Record<string, Planet>
|
planets: Record<string, Planet>
|
||||||
@@ -58,17 +164,26 @@ export const migrateGameData = (): void => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
delete oldData.universePlanets
|
delete oldData.universePlanets
|
||||||
|
needsSave = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 迁移残骸场数据
|
// 迁移残骸场数据
|
||||||
if (oldData.debrisFields) {
|
if (oldData.debrisFields) {
|
||||||
universeData.debrisFields = oldData.debrisFields
|
universeData.debrisFields = oldData.debrisFields
|
||||||
delete oldData.debrisFields
|
delete oldData.debrisFields
|
||||||
|
needsSave = true
|
||||||
}
|
}
|
||||||
// 保存迁移后的数据
|
|
||||||
|
// 保存universeStore数据
|
||||||
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
localStorage.setItem(universeStorageKey, encryptData(universeData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有任何数据被修改,保存gameStore数据
|
||||||
|
if (needsSave) {
|
||||||
localStorage.setItem(storageKey, encryptData(oldData))
|
localStorage.setItem(storageKey, encryptData(oldData))
|
||||||
|
console.log('[Migration] Game data migrated successfully')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('[Migration] Failed to migrate game data:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/utils/speed.ts
Normal file
28
src/utils/speed.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Resources } from '@/types/game'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按倍率缩放一个数值
|
||||||
|
* - 支持合法的 0(例如用于“暂停”)
|
||||||
|
*/
|
||||||
|
export const scaleNumber = (value: number, multiplier: number): number => value * multiplier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按倍率缩放 Resources(常用于“每小时产量/消耗”等展示)
|
||||||
|
* - 支持合法的 0(例如用于“暂停”)
|
||||||
|
*/
|
||||||
|
export const scaleResources = (resources: Resources, multiplier: number): Resources => ({
|
||||||
|
metal: resources.metal * multiplier,
|
||||||
|
crystal: resources.crystal * multiplier,
|
||||||
|
deuterium: resources.deuterium * multiplier,
|
||||||
|
darkMatter: resources.darkMatter * multiplier,
|
||||||
|
energy: resources.energy * multiplier
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算游戏循环的间隔(毫秒)
|
||||||
|
* - multiplier <= 0 或非有限值时,回退到 baseMs,避免除 0
|
||||||
|
*/
|
||||||
|
export const getGameLoopIntervalMs = (multiplier: number, baseMs: number = 1000): number => {
|
||||||
|
if (!Number.isFinite(multiplier) || multiplier <= 0) return baseMs
|
||||||
|
return baseMs / multiplier
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||||
<Card v-for="buildingType in availableBuildings" :key="buildingType" class="relative">
|
<Card v-for="buildingType in availableBuildings" :key="buildingType" :data-building="buildingType" class="relative">
|
||||||
<!-- 前置条件遮罩 -->
|
<!-- 前置条件遮罩 -->
|
||||||
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
<CardUnlockOverlay :requirements="BUILDINGS[buildingType].requirements" :currentLevel="getBuildingLevel(buildingType)" />
|
||||||
|
|
||||||
@@ -128,9 +128,18 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||||
{{ alertDialogMessage }}
|
{{ alertDialogMessage }}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogDescription v-else>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
|
||||||
|
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||||
|
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
||||||
|
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
@@ -179,7 +188,7 @@
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Clock, Grid3x3 } from 'lucide-vue-next'
|
import { Clock, Grid3x3, Check, X } from 'lucide-vue-next'
|
||||||
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
import { formatNumber, formatTime, getResourceCostColor } from '@/utils/format'
|
||||||
import * as buildingLogic from '@/logic/buildingLogic'
|
import * as buildingLogic from '@/logic/buildingLogic'
|
||||||
import * as buildingValidation from '@/logic/buildingValidation'
|
import * as buildingValidation from '@/logic/buildingValidation'
|
||||||
@@ -196,6 +205,8 @@
|
|||||||
const alertDialogOpen = ref(false)
|
const alertDialogOpen = ref(false)
|
||||||
const alertDialogTitle = ref('')
|
const alertDialogTitle = ref('')
|
||||||
const alertDialogMessage = ref('')
|
const alertDialogMessage = ref('')
|
||||||
|
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||||
|
const alertDialogShowRequirements = ref(false)
|
||||||
|
|
||||||
// 拆除确认对话框状态
|
// 拆除确认对话框状态
|
||||||
const demolishConfirmOpen = ref(false)
|
const demolishConfirmOpen = ref(false)
|
||||||
@@ -225,18 +236,18 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const upgradeBuilding = (buildingType: BuildingType): boolean => {
|
const upgradeBuilding = (buildingType: BuildingType): { success: boolean; reason?: string } => {
|
||||||
if (!gameStore.currentPlanet) return false
|
if (!gameStore.currentPlanet) return { success: false }
|
||||||
const validation = buildingValidation.validateBuildingUpgrade(
|
const validation = buildingValidation.validateBuildingUpgrade(
|
||||||
gameStore.currentPlanet,
|
gameStore.currentPlanet,
|
||||||
buildingType,
|
buildingType,
|
||||||
gameStore.player.technologies,
|
gameStore.player.technologies,
|
||||||
gameStore.player.officers
|
gameStore.player.officers
|
||||||
)
|
)
|
||||||
if (!validation.valid) return false
|
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||||
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 true
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsedSpace = (planet: Planet): number => {
|
const getUsedSpace = (planet: Planet): number => {
|
||||||
@@ -248,15 +259,18 @@
|
|||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(buildingType)) {
|
if (!checkUpgradeRequirements(buildingType)) {
|
||||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||||
alertDialogMessage.value = getRequirementsList(buildingType)
|
alertDialogRequirements.value = getRequirementsList(buildingType)
|
||||||
|
alertDialogShowRequirements.value = true
|
||||||
|
alertDialogMessage.value = ''
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = upgradeBuilding(buildingType)
|
const result = upgradeBuilding(buildingType)
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
alertDialogTitle.value = t('buildingsView.upgradeFailed')
|
||||||
alertDialogMessage.value = t('buildingsView.upgradeFailedMessage')
|
alertDialogMessage.value = result.reason ? t(result.reason) : t('buildingsView.upgradeFailedMessage')
|
||||||
|
alertDialogShowRequirements.value = false
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,18 +306,23 @@
|
|||||||
return t('buildingsView.maxLevelReached') // "等级已满"
|
return t('buildingsView.maxLevelReached') // "等级已满"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planet.value.buildQueue.length > 0) return t('buildingsView.upgrade')
|
// 0级为建造,1级及以上为升级
|
||||||
|
const buttonTextKey = currentLevel === 0 ? 'buildingsView.build' : 'buildingsView.upgrade'
|
||||||
|
|
||||||
|
if (planet.value.buildQueue.length > 0) return t(buttonTextKey)
|
||||||
|
|
||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(buildingType)) {
|
if (!checkUpgradeRequirements(buildingType)) {
|
||||||
return t('buildingsView.requirementsNotMet')
|
return t('buildingsView.requirementsNotMet')
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('buildingsView.upgrade')
|
return t(buttonTextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取前置条件列表文本
|
// 获取前置条件列表
|
||||||
const getRequirementsList = (buildingType: BuildingType): string => {
|
const getRequirementsList = (
|
||||||
|
buildingType: BuildingType
|
||||||
|
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
const config = BUILDINGS.value[buildingType]
|
const config = BUILDINGS.value[buildingType]
|
||||||
const currentLevel = getBuildingLevel(buildingType)
|
const currentLevel = getBuildingLevel(buildingType)
|
||||||
const targetLevel = currentLevel + 1
|
const targetLevel = currentLevel + 1
|
||||||
@@ -311,28 +330,26 @@
|
|||||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||||
|
|
||||||
if (!requirements || !planet.value) return ''
|
if (!requirements || !planet.value) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const bt = key as BuildingType
|
const bt = key as BuildingType
|
||||||
const currentLevel = planet.value.buildings[bt] || 0
|
const currentLevel = planet.value.buildings[bt] || 0
|
||||||
const name = BUILDINGS.value[bt]?.name || bt
|
const name = BUILDINGS.value[bt]?.name || bt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const tt = key as TechnologyType
|
const tt = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否可以升级
|
// 检查是否可以升级
|
||||||
|
|||||||
@@ -231,14 +231,14 @@
|
|||||||
return defenseType === DefenseType.SmallShieldDome || defenseType === DefenseType.LargeShieldDome
|
return defenseType === DefenseType.SmallShieldDome || defenseType === DefenseType.LargeShieldDome
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildDefense = (defenseType: DefenseType, quantity: number): boolean => {
|
const buildDefense = (defenseType: DefenseType, quantity: number): { success: boolean; reason?: string } => {
|
||||||
const currentPlanet = gameStore.currentPlanet
|
const currentPlanet = gameStore.currentPlanet
|
||||||
if (!currentPlanet) return 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 false
|
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||||
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 true
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建造防御设施
|
// 建造防御设施
|
||||||
@@ -251,10 +251,10 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = buildDefense(defenseType, quantity)
|
const result = buildDefense(defenseType, quantity)
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
alertDialogTitle.value = t('defenseView.buildFailed')
|
alertDialogTitle.value = t('defenseView.buildFailed')
|
||||||
alertDialogMessage.value = t('defenseView.buildFailedMessage')
|
alertDialogMessage.value = result.reason ? t(result.reason) : t('defenseView.buildFailedMessage')
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
} else {
|
} else {
|
||||||
quantities.value[defenseType] = 0
|
quantities.value[defenseType] = 0
|
||||||
|
|||||||
@@ -1,10 +1,181 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 视图切换和诊断按钮 -->
|
||||||
|
<div class="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<!-- 视图模式切换 -->
|
||||||
|
<div class="flex items-center border rounded-md">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 px-2 rounded-r-none"
|
||||||
|
:class="{ 'bg-accent': viewMode === 'list' }"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
:title="t('diplomacy.viewMode.list')"
|
||||||
|
>
|
||||||
|
<List class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 px-2 rounded-l-none border-l"
|
||||||
|
:class="{ 'bg-accent': viewMode === 'card' }"
|
||||||
|
@click="viewMode = 'card'"
|
||||||
|
:title="t('diplomacy.viewMode.card')"
|
||||||
|
>
|
||||||
|
<LayoutGrid class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<!-- NPC诊断按钮 -->
|
||||||
|
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||||
|
<Activity class="h-4 w-4 sm:mr-2" />
|
||||||
|
<span class="hidden sm:inline">{{ t('diplomacy.diagnostic.button') }}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NPC诊断对话框 -->
|
||||||
|
<Dialog v-model:open="npcDiagnosticOpen">
|
||||||
|
<ScrollableDialogContent container-class="max-w-4xl">
|
||||||
|
<template #header>
|
||||||
|
<DialogTitle>{{ t('diplomacy.diagnostic.title') }}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<div class="text-sm mt-2">
|
||||||
|
{{
|
||||||
|
t('diplomacy.diagnostic.description', {
|
||||||
|
points: gameStore.player.points || 0,
|
||||||
|
spyInterval: Math.floor(behaviorConfig.spyInterval / 60),
|
||||||
|
attackInterval: Math.floor(behaviorConfig.attackInterval / 60),
|
||||||
|
attackProb: (behaviorConfig.attackProbability * 100).toFixed(0)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 诊断对话框搜索框 -->
|
||||||
|
<div class="relative mb-4">
|
||||||
|
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input v-model="diagnosticSearchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="filteredDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">
|
||||||
|
{{ t('diplomacy.diagnostic.noData') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<div v-for="diagnostic in paginatedDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-bold text-lg">{{ diagnostic.npcName }}</h3>
|
||||||
|
<Badge :variant="getRelationBadgeVariant(diagnostic.relationStatusKey)">
|
||||||
|
{{ getLocalizedRelationStatus(diagnostic.relationStatusKey) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.difficulty') }}:</span>
|
||||||
|
<span class="font-medium">{{ t(`diplomacy.diagnostic.difficultyLevels.${diagnostic.difficulty}`) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.reputation') }}:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.spyProbes') }}:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.spyProbes }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.fleetPower') }}:</span>
|
||||||
|
<span class="font-medium">{{ diagnostic.totalFleetPower }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canSpy') }}:</span>
|
||||||
|
<span :class="diagnostic.canSpy ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||||
|
{{ diagnostic.canSpy ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canAttack') }}:</span>
|
||||||
|
<span :class="diagnostic.canAttack ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||||
|
{{ diagnostic.canAttack ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.attackProbability') }}:</span>
|
||||||
|
<span class="font-medium">{{ (diagnostic.attackProbability * 100).toFixed(0) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextSpy') }}:</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<template v-if="diagnostic.nextSpyIn > 0">
|
||||||
|
{{
|
||||||
|
t('diplomacy.diagnostic.timeFormat', { min: Math.floor(diagnostic.nextSpyIn / 60), sec: diagnostic.nextSpyIn % 60 })
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 flex items-center gap-2">
|
||||||
|
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextAttack') }}:</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
<template v-if="diagnostic.nextAttackIn > 0">
|
||||||
|
{{
|
||||||
|
t('diplomacy.diagnostic.timeFormat', {
|
||||||
|
min: Math.floor(diagnostic.nextAttackIn / 60),
|
||||||
|
sec: diagnostic.nextAttackIn % 60
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="diagnostic.reasons.length > 0" class="mt-3 p-3 bg-muted rounded text-xs">
|
||||||
|
<div class="font-semibold mb-2">{{ t('diplomacy.diagnostic.statusExplanation') }}:</div>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ translateReason(reason) }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页控制 -->
|
||||||
|
<Pagination
|
||||||
|
v-if="diagnosticTotalPages > 1"
|
||||||
|
v-model:page="diagnosticPage"
|
||||||
|
:total="filteredDiagnostics.length"
|
||||||
|
:items-per-page="DIAGNOSTIC_ITEMS_PER_PAGE"
|
||||||
|
:sibling-count="1"
|
||||||
|
show-edges
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||||
|
|
||||||
|
<template v-for="(pageNum, index) in diagnosticPageNumbers" :key="index">
|
||||||
|
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === diagnosticPage">
|
||||||
|
{{ pageNum }}
|
||||||
|
</PaginationItem>
|
||||||
|
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
</ScrollableDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="relative">
|
||||||
|
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input v-model="searchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关系状态过滤标签 -->
|
<!-- 关系状态过滤标签 -->
|
||||||
@@ -12,7 +183,12 @@
|
|||||||
<TabsList class="grid w-full grid-cols-4">
|
<TabsList class="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="all">
|
<TabsTrigger value="all">
|
||||||
{{ t('diplomacy.tabs.all') }}
|
{{ t('diplomacy.tabs.all') }}
|
||||||
<Badge variant="outline" class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700">{{ allNpcs.length }}</Badge>
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="ml-2 bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700"
|
||||||
|
>
|
||||||
|
{{ allNpcs.length }}
|
||||||
|
</Badge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="friendly">
|
<TabsTrigger value="friendly">
|
||||||
{{ t('diplomacy.tabs.friendly') }}
|
{{ t('diplomacy.tabs.friendly') }}
|
||||||
@@ -45,198 +221,273 @@
|
|||||||
|
|
||||||
<!-- 全部NPC -->
|
<!-- 全部NPC -->
|
||||||
<TabsContent value="all" class="space-y-4 mt-6">
|
<TabsContent value="all" class="space-y-4 mt-6">
|
||||||
<div v-if="allNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
<Empty v-if="allNpcs.length === 0" class="border rounded-lg">
|
||||||
{{ t('diplomacy.noNpcs') }}
|
<EmptyContent>
|
||||||
</div>
|
<Users class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('diplomacy.noNpcs') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- 卡片视图 -->
|
||||||
<NpcRelationCard v-for="npc in paginatedAllNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||||
|
<NpcRelationCard
|
||||||
|
v-for="npc in paginatedAllNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 列表视图 -->
|
||||||
|
<div v-else class="space-y-2 pb-20">
|
||||||
|
<NpcRelationRow
|
||||||
|
v-for="npc in paginatedAllNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
v-if="totalPagesAll > 1"
|
|
||||||
v-model:page="currentPage.all"
|
|
||||||
:total="allNpcs.length"
|
|
||||||
:items-per-page="ITEMS_PER_PAGE"
|
|
||||||
:sibling-count="1"
|
|
||||||
show-edges
|
|
||||||
class="mt-6"
|
|
||||||
>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
|
||||||
|
|
||||||
<template v-for="(pageNum, index) in pageNumbersAll" :key="index">
|
|
||||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.all">
|
|
||||||
{{ pageNum }}
|
|
||||||
</PaginationItem>
|
|
||||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</template>
|
</template>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 友好NPC -->
|
<!-- 友好NPC -->
|
||||||
<TabsContent value="friendly" class="space-y-4 mt-6">
|
<TabsContent value="friendly" class="space-y-4 mt-6">
|
||||||
<div v-if="friendlyNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
<Empty v-if="friendlyNpcs.length === 0" class="border rounded-lg">
|
||||||
{{ t('diplomacy.noFriendlyNpcs') }}
|
<EmptyContent>
|
||||||
</div>
|
<Heart class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('diplomacy.noFriendlyNpcs') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- 卡片视图 -->
|
||||||
<NpcRelationCard v-for="npc in paginatedFriendlyNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||||
|
<NpcRelationCard
|
||||||
|
v-for="npc in paginatedFriendlyNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 列表视图 -->
|
||||||
|
<div v-else class="space-y-2 pb-20">
|
||||||
|
<NpcRelationRow
|
||||||
|
v-for="npc in paginatedFriendlyNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
v-if="totalPagesFriendly > 1"
|
|
||||||
v-model:page="currentPage.friendly"
|
|
||||||
:total="friendlyNpcs.length"
|
|
||||||
:items-per-page="ITEMS_PER_PAGE"
|
|
||||||
:sibling-count="1"
|
|
||||||
show-edges
|
|
||||||
class="mt-6"
|
|
||||||
>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
|
||||||
|
|
||||||
<template v-for="(pageNum, index) in pageNumbersFriendly" :key="index">
|
|
||||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.friendly">
|
|
||||||
{{ pageNum }}
|
|
||||||
</PaginationItem>
|
|
||||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</template>
|
</template>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 中立NPC -->
|
<!-- 中立NPC -->
|
||||||
<TabsContent value="neutral" class="space-y-4 mt-6">
|
<TabsContent value="neutral" class="space-y-4 mt-6">
|
||||||
<div v-if="neutralNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
<Empty v-if="neutralNpcs.length === 0" class="border rounded-lg">
|
||||||
{{ t('diplomacy.noNeutralNpcs') }}
|
<EmptyContent>
|
||||||
</div>
|
<Minus class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('diplomacy.noNeutralNpcs') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- 卡片视图 -->
|
||||||
<NpcRelationCard v-for="npc in paginatedNeutralNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||||
|
<NpcRelationCard
|
||||||
|
v-for="npc in paginatedNeutralNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 列表视图 -->
|
||||||
|
<div v-else class="space-y-2 pb-20">
|
||||||
|
<NpcRelationRow
|
||||||
|
v-for="npc in paginatedNeutralNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
v-if="totalPagesNeutral > 1"
|
|
||||||
v-model:page="currentPage.neutral"
|
|
||||||
:total="neutralNpcs.length"
|
|
||||||
:items-per-page="ITEMS_PER_PAGE"
|
|
||||||
:sibling-count="1"
|
|
||||||
show-edges
|
|
||||||
class="mt-6"
|
|
||||||
>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
|
||||||
|
|
||||||
<template v-for="(pageNum, index) in pageNumbersNeutral" :key="index">
|
|
||||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.neutral">
|
|
||||||
{{ pageNum }}
|
|
||||||
</PaginationItem>
|
|
||||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</template>
|
</template>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 敌对NPC -->
|
<!-- 敌对NPC -->
|
||||||
<TabsContent value="hostile" class="space-y-4 mt-6">
|
<TabsContent value="hostile" class="space-y-4 mt-6">
|
||||||
<div v-if="hostileNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
<Empty v-if="hostileNpcs.length === 0" class="border rounded-lg">
|
||||||
{{ t('diplomacy.noHostileNpcs') }}
|
<EmptyContent>
|
||||||
</div>
|
<Swords class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('diplomacy.noHostileNpcs') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- 卡片视图 -->
|
||||||
<NpcRelationCard v-for="npc in paginatedHostileNpcs" :key="npc.id" :npc="npc" :relation="getRelation(npc.id)" />
|
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||||
|
<NpcRelationCard
|
||||||
|
v-for="npc in paginatedHostileNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 列表视图 -->
|
||||||
|
<div v-else class="space-y-2 pb-20">
|
||||||
|
<NpcRelationRow
|
||||||
|
v-for="npc in paginatedHostileNpcs"
|
||||||
|
:key="npc.id"
|
||||||
|
:ref="setCardRef(npc.id)"
|
||||||
|
:npc="npc"
|
||||||
|
:relation="getRelation(npc.id)"
|
||||||
|
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
v-if="totalPagesHostile > 1"
|
|
||||||
v-model:page="currentPage.hostile"
|
|
||||||
:total="hostileNpcs.length"
|
|
||||||
:items-per-page="ITEMS_PER_PAGE"
|
|
||||||
:sibling-count="1"
|
|
||||||
show-edges
|
|
||||||
class="mt-6"
|
|
||||||
>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
|
||||||
|
|
||||||
<template v-for="(pageNum, index) in pageNumbersHostile" :key="index">
|
|
||||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.hostile">
|
|
||||||
{{ pageNum }}
|
|
||||||
</PaginationItem>
|
|
||||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</template>
|
</template>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<!-- 外交报告历史 -->
|
<!-- 固定底部分页 -->
|
||||||
<Card v-if="diplomaticReports.length > 0">
|
<FixedPagination v-model:page="currentPageValue" :total-pages="currentTotalPages" />
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('diplomacy.recentEvents') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('diplomacy.recentEventsDescription') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
|
||||||
<div
|
|
||||||
v-for="report in diplomaticReports"
|
|
||||||
:key="report.id"
|
|
||||||
class="flex items-start gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="flex-shrink-0 mt-0.5">
|
|
||||||
<component :is="getEventIcon(report.eventType)" class="h-5 w-5" :class="getEventIconColor(report.reputationChange)" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center gap-2 mb-1">
|
|
||||||
<span class="font-medium">{{ report.npcName }}</span>
|
|
||||||
<Badge :variant="getReputationBadgeVariant(report.reputationChange)" class="text-xs">
|
|
||||||
{{ report.reputationChange > 0 ? '+' : '' }}{{ report.reputationChange }}
|
|
||||||
</Badge>
|
|
||||||
<Badge :variant="getStatusBadgeVariant(report.newStatus)" class="text-xs">
|
|
||||||
{{ getStatusText(report.newStatus) }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-muted-foreground">{{ report.message }}</p>
|
|
||||||
<p class="text-xs text-muted-foreground mt-1">{{ formatTime(Date.now() - report.timestamp) }} {{ t('diplomacy.ago') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from 'vue'
|
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useNPCStore } from '@/stores/npcStore'
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
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 { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||||
|
import {
|
||||||
|
FixedPagination,
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious
|
||||||
|
} from '@/components/ui/pagination'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||||
import { Gift, Sword, Eye, Trash2 } from 'lucide-vue-next'
|
import NpcRelationRow from '@/components/NpcRelationRow.vue'
|
||||||
import { RelationStatus, DiplomaticEventType } from '@/types/game'
|
import { RelationStatus } from '@/types/game'
|
||||||
import type { DiplomaticRelation, DiplomaticReport } from '@/types/game'
|
import type { DiplomaticRelation } from '@/types/game'
|
||||||
import { formatTime } from '@/utils/format'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
|
import { Search, Users, Heart, Minus, Swords, Activity, LayoutGrid, List } from 'lucide-vue-next'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 视图模式: 'card' | 'list'
|
||||||
|
const viewMode = ref<'card' | 'list'>('list')
|
||||||
|
|
||||||
|
// NPC卡片引用 Map(存储组件实例)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const cardRefs = ref<Map<string, any>>(new Map())
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const setCardRef = (npcId: string) => (el: any) => {
|
||||||
|
if (el) {
|
||||||
|
cardRefs.value.set(npcId, el)
|
||||||
|
} else {
|
||||||
|
cardRefs.value.delete(npcId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高亮状态
|
||||||
|
const highlightedNpcId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
// NPC诊断功能
|
||||||
|
const npcDiagnosticOpen = ref(false)
|
||||||
|
const npcDiagnostics = ref<npcBehaviorLogic.NPCDiagnosticInfo[]>([])
|
||||||
|
const diagnosticPage = ref(1)
|
||||||
|
const diagnosticSearchQuery = ref('')
|
||||||
|
const DIAGNOSTIC_ITEMS_PER_PAGE = 10
|
||||||
|
const behaviorConfig = computed(() => {
|
||||||
|
return npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showNPCDiagnostic = () => {
|
||||||
|
const currentTime = Date.now()
|
||||||
|
npcDiagnostics.value = npcBehaviorLogic.diagnoseNPCBehavior(npcStore.npcs, gameStore.player, currentTime)
|
||||||
|
diagnosticPage.value = 1 // 重置分页
|
||||||
|
diagnosticSearchQuery.value = '' // 重置搜索
|
||||||
|
npcDiagnosticOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊断搜索过滤
|
||||||
|
const filteredDiagnostics = computed(() => {
|
||||||
|
if (!diagnosticSearchQuery.value.trim()) return npcDiagnostics.value
|
||||||
|
const query = diagnosticSearchQuery.value.toLowerCase().trim()
|
||||||
|
return npcDiagnostics.value.filter(d => d.npcName.toLowerCase().includes(query) || d.npcId.toLowerCase().includes(query))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 诊断弹窗分页
|
||||||
|
const diagnosticTotalPages = computed(() => Math.ceil(filteredDiagnostics.value.length / DIAGNOSTIC_ITEMS_PER_PAGE))
|
||||||
|
const paginatedDiagnostics = computed(() => {
|
||||||
|
const start = (diagnosticPage.value - 1) * DIAGNOSTIC_ITEMS_PER_PAGE
|
||||||
|
const end = start + DIAGNOSTIC_ITEMS_PER_PAGE
|
||||||
|
return filteredDiagnostics.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
const diagnosticPageNumbers = computed(() => getPageNumbers(diagnosticPage.value, diagnosticTotalPages.value))
|
||||||
|
|
||||||
|
// 诊断面板关系状态本地化
|
||||||
|
const getLocalizedRelationStatus = (statusKey: string) => {
|
||||||
|
switch (statusKey) {
|
||||||
|
case 'friendly':
|
||||||
|
return t('diplomacy.status.friendly')
|
||||||
|
case 'hostile':
|
||||||
|
return t('diplomacy.status.hostile')
|
||||||
|
case 'neutral':
|
||||||
|
return t('diplomacy.status.neutral')
|
||||||
|
case 'noRelation':
|
||||||
|
return t('diplomacy.diagnostic.noRelation')
|
||||||
|
case 'noRelationNeutral':
|
||||||
|
return t('diplomacy.diagnostic.noRelationNeutral')
|
||||||
|
default:
|
||||||
|
return t('diplomacy.status.neutral')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 诊断面板关系Badge样式
|
||||||
|
const getRelationBadgeVariant = (statusKey: string) => {
|
||||||
|
switch (statusKey) {
|
||||||
|
case 'friendly':
|
||||||
|
return 'default'
|
||||||
|
case 'hostile':
|
||||||
|
return 'destructive'
|
||||||
|
default:
|
||||||
|
return 'secondary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻译诊断原因
|
||||||
|
const translateReason = (reason: { key: string; params?: Record<string, string | number> }) => {
|
||||||
|
const translationKey = `diplomacy.diagnostic.reasons.${reason.key}`
|
||||||
|
return reason.params ? t(translationKey, reason.params) : t(translationKey)
|
||||||
|
}
|
||||||
|
|
||||||
// 检测并生成NPC盟友
|
// 检测并生成NPC盟友
|
||||||
const initializeNPCAllies = () => {
|
const initializeNPCAllies = () => {
|
||||||
const npcs = npcStore.npcs
|
const npcs = npcStore.npcs
|
||||||
@@ -278,9 +529,61 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到指定NPC卡片
|
||||||
|
const scrollToNpcCard = (npcId: string) => {
|
||||||
|
// 切换到"全部"标签
|
||||||
|
activeTab.value = 'all'
|
||||||
|
|
||||||
|
// 等待DOM更新后再滚动
|
||||||
|
nextTick(() => {
|
||||||
|
// 找到目标NPC在列表中的索引
|
||||||
|
const npcIndex = allNpcs.value.findIndex(npc => npc.id === npcId)
|
||||||
|
if (npcIndex === -1) return
|
||||||
|
|
||||||
|
// 计算目标NPC所在的页面
|
||||||
|
const targetPage = Math.floor(npcIndex / ITEMS_PER_PAGE) + 1
|
||||||
|
currentPage.value.all = targetPage
|
||||||
|
|
||||||
|
// 再次等待分页更新后滚动到卡片
|
||||||
|
nextTick(() => {
|
||||||
|
// 从 cardRefs 获取组件实例
|
||||||
|
const cardComponent = cardRefs.value.get(npcId)
|
||||||
|
const targetEl = cardComponent?.$el as HTMLElement | undefined
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
// 添加高亮效果
|
||||||
|
highlightedNpcId.value = npcId
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightedNpcId.value = null
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 组件挂载时初始化NPC盟友
|
// 组件挂载时初始化NPC盟友
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initializeNPCAllies()
|
initializeNPCAllies()
|
||||||
|
|
||||||
|
// 检查URL query参数,如果有npcId则滚动到该NPC
|
||||||
|
const npcIdFromQuery = route.query.npcId as string | undefined
|
||||||
|
if (npcIdFromQuery) {
|
||||||
|
scrollToNpcCard(npcIdFromQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听滚动到NPC卡片的事件
|
||||||
|
const handleScrollToNpc = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<{ npcId: string }>
|
||||||
|
scrollToNpcCard(customEvent.detail.npcId)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('scrollToNpc', handleScrollToNpc)
|
||||||
|
|
||||||
|
// 清理事件监听器
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('scrollToNpc', handleScrollToNpc)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分页状态
|
// 分页状态
|
||||||
@@ -292,16 +595,25 @@
|
|||||||
hostile: 1
|
hostile: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取玩家对NPC的关系
|
// 获取NPC对玩家的关系(统一使用 npc.relations)
|
||||||
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
||||||
return gameStore.player.diplomaticRelations?.[npcId]
|
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||||
|
return npc?.relations?.[gameStore.player.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按关系状态分类NPC
|
// 搜索过滤函数
|
||||||
const allNpcs = computed(() => npcStore.npcs)
|
const matchesSearch = (npc: (typeof npcStore.npcs)[0]) => {
|
||||||
|
if (!searchQuery.value.trim()) return true
|
||||||
|
const query = searchQuery.value.toLowerCase().trim()
|
||||||
|
return npc.name.toLowerCase().includes(query) || npc.id.toLowerCase().includes(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按关系状态分类NPC(同时应用搜索过滤)
|
||||||
|
const allNpcs = computed(() => npcStore.npcs.filter(matchesSearch))
|
||||||
|
|
||||||
const friendlyNpcs = computed(() => {
|
const friendlyNpcs = computed(() => {
|
||||||
return npcStore.npcs.filter(npc => {
|
return npcStore.npcs.filter(npc => {
|
||||||
|
if (!matchesSearch(npc)) return false
|
||||||
const relation = getRelation(npc.id)
|
const relation = getRelation(npc.id)
|
||||||
return relation?.status === RelationStatus.Friendly
|
return relation?.status === RelationStatus.Friendly
|
||||||
})
|
})
|
||||||
@@ -309,6 +621,7 @@
|
|||||||
|
|
||||||
const neutralNpcs = computed(() => {
|
const neutralNpcs = computed(() => {
|
||||||
return npcStore.npcs.filter(npc => {
|
return npcStore.npcs.filter(npc => {
|
||||||
|
if (!matchesSearch(npc)) return false
|
||||||
const relation = getRelation(npc.id)
|
const relation = getRelation(npc.id)
|
||||||
return !relation || relation.status === RelationStatus.Neutral
|
return !relation || relation.status === RelationStatus.Neutral
|
||||||
})
|
})
|
||||||
@@ -316,6 +629,7 @@
|
|||||||
|
|
||||||
const hostileNpcs = computed(() => {
|
const hostileNpcs = computed(() => {
|
||||||
return npcStore.npcs.filter(npc => {
|
return npcStore.npcs.filter(npc => {
|
||||||
|
if (!matchesSearch(npc)) return false
|
||||||
const relation = getRelation(npc.id)
|
const relation = getRelation(npc.id)
|
||||||
return relation?.status === RelationStatus.Hostile
|
return relation?.status === RelationStatus.Hostile
|
||||||
})
|
})
|
||||||
@@ -345,107 +659,73 @@
|
|||||||
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
||||||
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
||||||
|
|
||||||
// 生成页码列表(用于分页UI)
|
// 生成页码列表(用于诊断弹窗分页UI)
|
||||||
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
||||||
const pages: (number | string)[] = []
|
const pages: number[] = []
|
||||||
const maxVisible = 5 // 最多显示5个页码
|
const maxVisible = 3
|
||||||
|
|
||||||
if (totalPages <= maxVisible) {
|
if (totalPages <= maxVisible) {
|
||||||
// 如果总页数少于等于5,显示全部
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
pages.push(i)
|
pages.push(i)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 总是显示第1页
|
let start = currentPageNum - 1
|
||||||
pages.push(1)
|
let end = currentPageNum + 1
|
||||||
|
|
||||||
if (currentPageNum > 3) {
|
if (start < 1) {
|
||||||
pages.push('...')
|
start = 1
|
||||||
|
end = maxVisible
|
||||||
|
}
|
||||||
|
if (end > totalPages) {
|
||||||
|
end = totalPages
|
||||||
|
start = totalPages - maxVisible + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算中间显示的页码范围
|
|
||||||
const start = Math.max(2, currentPageNum - 1)
|
|
||||||
const end = Math.min(totalPages - 1, currentPageNum + 1)
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
pages.push(i)
|
pages.push(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPageNum < totalPages - 2) {
|
|
||||||
pages.push('...')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 总是显示最后一页
|
|
||||||
pages.push(totalPages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pages
|
return pages
|
||||||
}
|
}
|
||||||
|
|
||||||
// 各标签页的页码列表
|
// 当前标签页的分页数据(用于固定底部分页)
|
||||||
const pageNumbersAll = computed(() => getPageNumbers(currentPage.value.all || 1, totalPagesAll.value))
|
const currentTotalPages = computed(() => {
|
||||||
const pageNumbersFriendly = computed(() => getPageNumbers(currentPage.value.friendly || 1, totalPagesFriendly.value))
|
switch (activeTab.value) {
|
||||||
const pageNumbersNeutral = computed(() => getPageNumbers(currentPage.value.neutral || 1, totalPagesNeutral.value))
|
case 'friendly':
|
||||||
const pageNumbersHostile = computed(() => getPageNumbers(currentPage.value.hostile || 1, totalPagesHostile.value))
|
return totalPagesFriendly.value
|
||||||
|
case 'neutral':
|
||||||
// 外交报告(最近20条,按时间倒序)
|
return totalPagesNeutral.value
|
||||||
const diplomaticReports = computed(() => {
|
case 'hostile':
|
||||||
const reports = gameStore.player.diplomaticReports || []
|
return totalPagesHostile.value
|
||||||
return [...reports].sort((a, b) => b.timestamp - a.timestamp).slice(0, 20)
|
default:
|
||||||
|
return totalPagesAll.value
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取事件图标
|
const currentPageValue = computed({
|
||||||
const getEventIcon = (eventType: DiplomaticReport['eventType']) => {
|
get: () => currentPage.value[activeTab.value] || 1,
|
||||||
switch (eventType) {
|
set: (val: number) => {
|
||||||
case DiplomaticEventType.GiftResources:
|
currentPage.value[activeTab.value] = val
|
||||||
return Gift
|
|
||||||
case DiplomaticEventType.Attack:
|
|
||||||
case DiplomaticEventType.AllyAttacked:
|
|
||||||
return Sword
|
|
||||||
case DiplomaticEventType.Spy:
|
|
||||||
return Eye
|
|
||||||
case DiplomaticEventType.StealDebris:
|
|
||||||
return Trash2
|
|
||||||
default:
|
|
||||||
return Gift
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取事件图标颜色
|
|
||||||
const getEventIconColor = (reputationChange: number) => {
|
|
||||||
if (reputationChange > 0) return 'text-green-600 dark:text-green-400'
|
|
||||||
if (reputationChange < 0) return 'text-red-600 dark:text-red-400'
|
|
||||||
return 'text-muted-foreground'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取好感度Badge样式
|
|
||||||
const getReputationBadgeVariant = (change: number) => {
|
|
||||||
if (change > 0) return 'default'
|
|
||||||
if (change < 0) return 'destructive'
|
|
||||||
return 'secondary'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取关系状态Badge样式
|
|
||||||
const getStatusBadgeVariant = (status: RelationStatus) => {
|
|
||||||
switch (status) {
|
|
||||||
case RelationStatus.Friendly:
|
|
||||||
return 'default'
|
|
||||||
case RelationStatus.Hostile:
|
|
||||||
return 'destructive'
|
|
||||||
default:
|
|
||||||
return 'secondary'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取关系状态文本
|
|
||||||
const getStatusText = (status: RelationStatus) => {
|
|
||||||
switch (status) {
|
|
||||||
case RelationStatus.Friendly:
|
|
||||||
return t('diplomacy.status.friendly')
|
|
||||||
case RelationStatus.Hostile:
|
|
||||||
return t('diplomacy.status.hostile')
|
|
||||||
default:
|
|
||||||
return t('diplomacy.status.neutral')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* NPC卡片高亮动画 - 不使用scoped以便动态添加类生效 */
|
||||||
|
.npc-highlight {
|
||||||
|
animation: highlight-pulse 1.5s ease-in-out 2 !important;
|
||||||
|
box-shadow: 0 0 0 3px var(--primary) !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes highlight-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 3px var(--primary);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 6px var(--primary), 0 0 25px var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -8,11 +8,9 @@
|
|||||||
<!-- 标签切换 -->
|
<!-- 标签切换 -->
|
||||||
<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 grid-cols-3">
|
||||||
<TabsTrigger value="fleet">{{ t('fleetView.fleetOverview') }}</TabsTrigger>
|
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value">
|
||||||
<TabsTrigger value="send">{{ t('fleetView.sendFleet') }}</TabsTrigger>
|
{{ t(`fleetView.${tab.labelKey}`) }}
|
||||||
<TabsTrigger value="missions">
|
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||||
{{ t('fleetView.flightMissions') }}
|
|
||||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
|
||||||
{{ gameStore.player.fleetMissions.length }}
|
{{ gameStore.player.fleetMissions.length }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -97,17 +95,9 @@
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||||
<div class="space-y-2">
|
<div v-for="coord in coordinateFields" :key="coord.key" class="space-y-2">
|
||||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
<Label :for="coord.key" class="text-xs sm:text-sm">{{ t(`fleetView.${coord.key}`) }}</Label>
|
||||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
<Input :id="coord.key" v-model.number="targetPosition[coord.key]" type="number" :min="1" :max="coord.max" placeholder="1" />
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
|
||||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
|
||||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -156,39 +146,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||||
<div class="space-y-2">
|
<div v-for="res in cargoResourceFields" :key="res.key" class="space-y-2">
|
||||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
<Label :for="`cargo-${res.key}`" class="text-xs sm:text-sm flex items-center gap-2">
|
||||||
<ResourceIcon type="metal" size="sm" />
|
<ResourceIcon :type="res.key" size="sm" />
|
||||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
{{ t(`resources.${res.key}`) }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources[res.key]) }})
|
||||||
</Label>
|
|
||||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
|
||||||
<ResourceIcon type="crystal" size="sm" />
|
|
||||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="cargo-crystal"
|
:id="`cargo-${res.key}`"
|
||||||
v-model.number="cargo.crystal"
|
v-model.number="cargo[res.key]"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
:max="planet.resources.crystal"
|
:max="planet.resources[res.key]"
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
|
||||||
<ResourceIcon type="deuterium" size="sm" />
|
|
||||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="cargo-deuterium"
|
|
||||||
v-model.number="cargo.deuterium"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
:max="planet.resources.deuterium"
|
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,9 +198,12 @@
|
|||||||
|
|
||||||
<!-- 飞行任务 -->
|
<!-- 飞行任务 -->
|
||||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
<Empty v-if="gameStore.player.fleetMissions.length === 0" class="border rounded-lg">
|
||||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
<EmptyContent>
|
||||||
</Card>
|
<RocketIcon class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('fleetView.noFlightMissions') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
|
||||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -261,25 +233,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 携带资源 -->
|
<!-- 携带资源 -->
|
||||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
<div v-if="hasCargoResources(mission.cargo)">
|
||||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||||
<div class="flex flex-wrap gap-2 text-xs">
|
<div class="flex flex-wrap gap-2 text-xs">
|
||||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
<template v-for="res in cargoResourceFields" :key="res.key">
|
||||||
<ResourceIcon type="metal" size="sm" />
|
<span v-if="mission.cargo[res.key] > 0" class="flex items-center gap-1">
|
||||||
{{ formatNumber(mission.cargo.metal) }}
|
<ResourceIcon :type="res.key" size="sm" />
|
||||||
</span>
|
{{ formatNumber(mission.cargo[res.key]) }}
|
||||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="crystal" size="sm" />
|
|
||||||
{{ formatNumber(mission.cargo.crystal) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="deuterium" size="sm" />
|
|
||||||
{{ formatNumber(mission.cargo.deuterium) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
|
||||||
<ResourceIcon type="darkMatter" size="sm" />
|
|
||||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -343,8 +305,8 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { useGameConfig } from '@/composables/useGameConfig'
|
import { useGameConfig } from '@/composables/useGameConfig'
|
||||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ShipType, MissionType, BuildingType } from '@/types/game'
|
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
|
||||||
import type { Fleet, Resources } from '@/types/game'
|
import type { Fleet, Resources } 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'
|
||||||
@@ -366,7 +328,8 @@
|
|||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift } from 'lucide-vue-next'
|
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 { 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'
|
||||||
@@ -376,7 +339,6 @@
|
|||||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const universeStore = useUniverseStore()
|
const universeStore = useUniverseStore()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
@@ -397,11 +359,19 @@
|
|||||||
// 计算最大舰队任务槽位
|
// 计算最大舰队任务槽位
|
||||||
const maxFleetMissions = computed(() => {
|
const maxFleetMissions = computed(() => {
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||||
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots)
|
const computerTechLevel = gameStore.player.technologies[TechnologyType.ComputerTechnology] || 0
|
||||||
|
return publicLogic.getMaxFleetMissions(bonuses.additionalFleetSlots, computerTechLevel)
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||||
|
|
||||||
|
// Tab 配置
|
||||||
|
const fleetTabs = [
|
||||||
|
{ value: 'fleet', labelKey: 'fleetOverview' },
|
||||||
|
{ value: 'send', labelKey: 'sendFleet' },
|
||||||
|
{ value: 'missions', labelKey: 'flightMissions' }
|
||||||
|
] as const
|
||||||
|
|
||||||
// 选择的舰队
|
// 选择的舰队
|
||||||
const selectedFleet = ref<Partial<Fleet>>({
|
const selectedFleet = ref<Partial<Fleet>>({
|
||||||
[ShipType.LightFighter]: 0,
|
[ShipType.LightFighter]: 0,
|
||||||
@@ -420,12 +390,23 @@
|
|||||||
// 目标坐标
|
// 目标坐标
|
||||||
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
|
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
|
||||||
|
|
||||||
|
// 坐标字段配置
|
||||||
|
const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
|
||||||
|
{ key: 'galaxy', max: 9 },
|
||||||
|
{ key: 'system', max: 10 },
|
||||||
|
{ key: 'position', max: 10 }
|
||||||
|
]
|
||||||
|
|
||||||
// 选择的任务类型
|
// 选择的任务类型
|
||||||
const selectedMission = ref<MissionType>(MissionType.Attack)
|
const selectedMission = ref<MissionType>(MissionType.Attack)
|
||||||
|
|
||||||
// 运输资源
|
// 运输资源
|
||||||
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
||||||
|
|
||||||
|
// 货物资源字段配置
|
||||||
|
type CargoKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||||
|
const cargoResourceFields: { key: CargoKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
|
||||||
|
|
||||||
// 从 URL query 参数初始化
|
// 从 URL query 参数初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 启动定时器更新当前时间
|
// 启动定时器更新当前时间
|
||||||
@@ -457,9 +438,6 @@
|
|||||||
|
|
||||||
// 自动切换到派遣舰队标签
|
// 自动切换到派遣舰队标签
|
||||||
activeTab.value = 'send'
|
activeTab.value = 'send'
|
||||||
|
|
||||||
// 清除 URL 参数,保持 URL 整洁
|
|
||||||
router.replace({ path: '/fleet' })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -504,6 +482,7 @@
|
|||||||
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
{ type: MissionType.Colonize, name: t('fleetView.colonize'), icon: RocketIcon },
|
||||||
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
{ type: MissionType.Spy, name: t('fleetView.spy'), icon: Eye },
|
||||||
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
{ type: MissionType.Deploy, name: t('fleetView.deploy'), icon: Users },
|
||||||
|
{ type: MissionType.Expedition, name: t('fleetView.expedition'), icon: Compass },
|
||||||
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
{ type: MissionType.Recycle, name: t('fleetView.recycle'), icon: Recycle },
|
||||||
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
{ type: MissionType.Destroy, name: t('fleetView.destroy'), icon: Skull }
|
||||||
])
|
])
|
||||||
@@ -537,6 +516,11 @@
|
|||||||
return cargo.value.metal + cargo.value.crystal + cargo.value.deuterium + cargo.value.darkMatter
|
return cargo.value.metal + cargo.value.crystal + cargo.value.deuterium + cargo.value.darkMatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否有携带资源
|
||||||
|
const hasCargoResources = (cargoData: Resources): boolean => {
|
||||||
|
return cargoData.metal > 0 || cargoData.crystal > 0 || cargoData.deuterium > 0 || cargoData.darkMatter > 0
|
||||||
|
}
|
||||||
|
|
||||||
// 计算燃料消耗(包含货物重量影响)
|
// 计算燃料消耗(包含货物重量影响)
|
||||||
const getFuelConsumption = (): number => {
|
const getFuelConsumption = (): number => {
|
||||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||||
@@ -616,7 +600,8 @@
|
|||||||
fleet,
|
fleet,
|
||||||
cargo,
|
cargo,
|
||||||
gameStore.player.officers,
|
gameStore.player.officers,
|
||||||
currentMissions
|
currentMissions,
|
||||||
|
gameStore.player.technologies
|
||||||
)
|
)
|
||||||
if (!validation.valid) return false
|
if (!validation.valid) return false
|
||||||
const shouldDeductCargo = missionType === MissionType.Transport
|
const shouldDeductCargo = missionType === MissionType.Transport
|
||||||
|
|||||||
@@ -77,110 +77,36 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 建筑 -->
|
<!-- 建筑/科技/舰船/防御/军官 - 统一配置渲染 -->
|
||||||
<TabsContent value="buildings" class="space-y-4">
|
<TabsContent v-for="section in gmSections" :key="section.tabValue" :value="section.tabValue" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
|
<CardTitle>{{ t(section.titleKey) }}</CardTitle>
|
||||||
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
|
<CardDescription>{{ t(section.descKey) }}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
|
<div v-for="item in section.items" :key="item" class="space-y-2">
|
||||||
<Label>{{ BUILDINGS[building].name }}</Label>
|
<Label>{{ section.getItemName(item) }}</Label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
|
<Input
|
||||||
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
|
:model-value="section.getValue(item)"
|
||||||
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
|
@update:model-value="section.setValue(item, Number($event) || 0)"
|
||||||
</div>
|
type="number"
|
||||||
</div>
|
:min="0"
|
||||||
</div>
|
:max="section.max"
|
||||||
</CardContent>
|
:placeholder="section.placeholder"
|
||||||
</Card>
|
class="flex-1"
|
||||||
</TabsContent>
|
/>
|
||||||
|
<Button
|
||||||
<!-- 科技 -->
|
v-for="btn in section.buttons"
|
||||||
<TabsContent value="research" class="space-y-4">
|
:key="btn.label"
|
||||||
<Card>
|
@click="section.onButtonClick(item, btn.value)"
|
||||||
<CardHeader>
|
variant="outline"
|
||||||
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
|
size="sm"
|
||||||
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
|
>
|
||||||
</CardHeader>
|
{{ btn.label }}
|
||||||
<CardContent>
|
</Button>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
|
|
||||||
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
|
|
||||||
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
|
|
||||||
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<!-- 舰船 -->
|
|
||||||
<TabsContent value="ships" class="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
|
|
||||||
<Label>{{ SHIPS[ship].name }}</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
|
|
||||||
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
|
|
||||||
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<!-- 防御 -->
|
|
||||||
<TabsContent value="defense" class="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
|
|
||||||
<Label>{{ DEFENSES[defense].name }}</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
|
|
||||||
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
|
|
||||||
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<!-- 军官 -->
|
|
||||||
<TabsContent value="officers" class="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
|
|
||||||
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
|
|
||||||
<Label>{{ OFFICERS[officer].name }}</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
|
|
||||||
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
|
|
||||||
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
|
|
||||||
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,7 +130,9 @@
|
|||||||
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
|
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
|
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">
|
||||||
|
{{ npc.name }} ({{ t(`diplomacy.diagnostic.difficultyLevels.${npc.difficulty}`) }})
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,6 +221,9 @@
|
|||||||
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
|
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
|
||||||
{{ alertDialogMessage }}
|
{{ alertDialogMessage }}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogDescription v-else class="sr-only">
|
||||||
|
{{ alertDialogTitle }}
|
||||||
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
@@ -331,6 +262,7 @@
|
|||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game'
|
||||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||||
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import { Home } from 'lucide-vue-next'
|
import { Home } from 'lucide-vue-next'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -340,6 +272,11 @@
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
const { BUILDINGS, TECHNOLOGIES, SHIPS, DEFENSES, OFFICERS } = useGameConfig()
|
||||||
|
|
||||||
|
// 更新玩家积分的辅助函数
|
||||||
|
const updatePlayerPoints = () => {
|
||||||
|
gameStore.player.points = publicLogic.calculatePlayerPoints(gameStore.player)
|
||||||
|
}
|
||||||
|
|
||||||
const goHome = () => {
|
const goHome = () => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
@@ -382,11 +319,6 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||||
const buildingTypes = Object.values(BuildingType)
|
|
||||||
const technologyTypes = Object.values(TechnologyType)
|
|
||||||
const shipTypes = Object.values(ShipType)
|
|
||||||
const defenseTypes = Object.values(DefenseType)
|
|
||||||
const officerTypes = Object.values(OfficerType)
|
|
||||||
|
|
||||||
// Tab配置
|
// Tab配置
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -404,48 +336,161 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
// GM编辑区块配置 - 统一管理建筑/科技/舰船/防御/军官
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type GMSection = {
|
||||||
|
tabValue: string
|
||||||
|
titleKey: string
|
||||||
|
descKey: string
|
||||||
|
items: string[]
|
||||||
|
max?: number
|
||||||
|
placeholder?: string
|
||||||
|
buttons: { label: string; value: number }[]
|
||||||
|
getItemName: (item: any) => string
|
||||||
|
getValue: (item: any) => number
|
||||||
|
setValue: (item: any, val: number) => void
|
||||||
|
onButtonClick: (item: any, val: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const gmSections = computed<GMSection[]>(() => [
|
||||||
|
{
|
||||||
|
tabValue: 'buildings',
|
||||||
|
titleKey: 'gmView.modifyBuildings',
|
||||||
|
descKey: 'gmView.buildingsDesc',
|
||||||
|
items: Object.values(BuildingType),
|
||||||
|
max: 100,
|
||||||
|
placeholder: undefined,
|
||||||
|
buttons: [
|
||||||
|
{ label: 'Lv 10', value: 10 },
|
||||||
|
{ label: 'Lv 30', value: 30 }
|
||||||
|
],
|
||||||
|
getItemName: (item: BuildingType) => BUILDINGS.value[item].name,
|
||||||
|
getValue: (item: BuildingType) => selectedPlanet.value?.buildings[item] || 0,
|
||||||
|
setValue: (item: BuildingType, val: number) => {
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.buildings[building] = level
|
selectedPlanet.value.buildings[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onButtonClick: (item: BuildingType, val: number) => {
|
||||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
|
||||||
gameStore.player.technologies[tech] = level
|
|
||||||
}
|
|
||||||
|
|
||||||
const setShipCount = (ship: ShipType, count: number) => {
|
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
selectedPlanet.value.buildings[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
{
|
||||||
|
tabValue: 'research',
|
||||||
|
titleKey: 'gmView.modifyResearch',
|
||||||
|
descKey: 'gmView.researchDesc',
|
||||||
|
items: Object.values(TechnologyType),
|
||||||
|
max: 50,
|
||||||
|
placeholder: undefined,
|
||||||
|
buttons: [
|
||||||
|
{ label: 'Lv 10', value: 10 },
|
||||||
|
{ label: 'Lv 20', value: 20 }
|
||||||
|
],
|
||||||
|
getItemName: (item: TechnologyType) => TECHNOLOGIES.value[item].name,
|
||||||
|
getValue: (item: TechnologyType) => gameStore.player.technologies[item] || 0,
|
||||||
|
setValue: (item: TechnologyType, val: number) => {
|
||||||
|
gameStore.player.technologies[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
|
},
|
||||||
|
onButtonClick: (item: TechnologyType, val: number) => {
|
||||||
|
gameStore.player.technologies[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tabValue: 'ships',
|
||||||
|
titleKey: 'gmView.modifyShips',
|
||||||
|
descKey: 'gmView.shipsDesc',
|
||||||
|
items: Object.values(ShipType),
|
||||||
|
max: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
buttons: [
|
||||||
|
{ label: '+100', value: 100 },
|
||||||
|
{ label: '+1K', value: 1000 }
|
||||||
|
],
|
||||||
|
getItemName: (item: ShipType) => SHIPS.value[item].name,
|
||||||
|
getValue: (item: ShipType) => selectedPlanet.value?.fleet[item] || 0,
|
||||||
|
setValue: (item: ShipType, val: number) => {
|
||||||
if (selectedPlanet.value) {
|
if (selectedPlanet.value) {
|
||||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
selectedPlanet.value.fleet[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onButtonClick: (item: ShipType, val: number) => {
|
||||||
|
if (selectedPlanet.value) {
|
||||||
|
selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val
|
||||||
|
updatePlayerPoints()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const setOfficerDays = (officer: OfficerType, days: number) => {
|
{
|
||||||
officerDays.value[officer] = days
|
tabValue: 'defense',
|
||||||
|
titleKey: 'gmView.modifyDefense',
|
||||||
|
descKey: 'gmView.defenseDesc',
|
||||||
|
items: Object.values(DefenseType),
|
||||||
|
max: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
buttons: [
|
||||||
|
{ label: '+100', value: 100 },
|
||||||
|
{ label: '+1K', value: 1000 }
|
||||||
|
],
|
||||||
|
getItemName: (item: DefenseType) => DEFENSES.value[item].name,
|
||||||
|
getValue: (item: DefenseType) => selectedPlanet.value?.defense[item] || 0,
|
||||||
|
setValue: (item: DefenseType, val: number) => {
|
||||||
|
if (selectedPlanet.value) {
|
||||||
|
selectedPlanet.value.defense[item] = val
|
||||||
|
updatePlayerPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onButtonClick: (item: DefenseType, val: number) => {
|
||||||
|
if (selectedPlanet.value) {
|
||||||
|
selectedPlanet.value.defense[item] = (selectedPlanet.value.defense[item] || 0) + val
|
||||||
|
updatePlayerPoints()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tabValue: 'officers',
|
||||||
|
titleKey: 'gmView.modifyOfficers',
|
||||||
|
descKey: 'gmView.officersDesc',
|
||||||
|
items: Object.values(OfficerType),
|
||||||
|
max: undefined,
|
||||||
|
placeholder: t('gmView.days'),
|
||||||
|
buttons: [
|
||||||
|
{ label: `7${t('gmView.days')}`, value: 7 },
|
||||||
|
{ label: `30${t('gmView.days')}`, value: 30 },
|
||||||
|
{ label: `365${t('gmView.days')}`, value: 365 }
|
||||||
|
],
|
||||||
|
getItemName: (item: OfficerType) => OFFICERS.value[item].name,
|
||||||
|
getValue: (item: OfficerType) => officerDays.value[item] || 0,
|
||||||
|
setValue: (item: OfficerType, val: number) => {
|
||||||
|
officerDays.value[item] = val
|
||||||
|
},
|
||||||
|
onButtonClick: (item: OfficerType, days: number) => {
|
||||||
|
officerDays.value[item] = days
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
||||||
|
if (!gameStore.player.officers[item]) {
|
||||||
if (!gameStore.player.officers[officer]) {
|
gameStore.player.officers[item] = {
|
||||||
gameStore.player.officers[officer] = {
|
type: item,
|
||||||
type: officer,
|
|
||||||
active: true,
|
active: true,
|
||||||
hiredAt: now,
|
hiredAt: now,
|
||||||
expiresAt: expiresAt
|
expiresAt: expiresAt
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gameStore.player.officers[officer].expiresAt = expiresAt
|
gameStore.player.officers[item].expiresAt = expiresAt
|
||||||
gameStore.player.officers[officer].active = true
|
gameStore.player.officers[item].active = true
|
||||||
if (!gameStore.player.officers[officer].hiredAt) {
|
if (!gameStore.player.officers[item].hiredAt) {
|
||||||
gameStore.player.officers[officer].hiredAt = now
|
gameStore.player.officers[item].hiredAt = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// 显示重置游戏确认对话框
|
// 显示重置游戏确认对话框
|
||||||
const showResetConfirmDialog = () => {
|
const showResetConfirmDialog = () => {
|
||||||
@@ -614,7 +659,7 @@
|
|||||||
const maxAllResources = () => {
|
const maxAllResources = () => {
|
||||||
if (!selectedPlanet.value) return
|
if (!selectedPlanet.value) return
|
||||||
|
|
||||||
const maxAmount = 1000000000 // 10亿
|
const maxAmount = 1000000000000000000
|
||||||
selectedPlanet.value.resources.metal = maxAmount
|
selectedPlanet.value.resources.metal = maxAmount
|
||||||
selectedPlanet.value.resources.crystal = maxAmount
|
selectedPlanet.value.resources.crystal = maxAmount
|
||||||
selectedPlanet.value.resources.deuterium = maxAmount
|
selectedPlanet.value.resources.deuterium = maxAmount
|
||||||
@@ -708,6 +753,9 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 更新玩家积分(因为建筑/科技/舰队/防御可能已改变)
|
||||||
|
updatePlayerPoints()
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t('gmView.completeQueuesSuccess', {
|
t('gmView.completeQueuesSuccess', {
|
||||||
buildingCount,
|
buildingCount,
|
||||||
|
|||||||
@@ -205,16 +205,30 @@
|
|||||||
<div v-if="slot.planet" class="space-y-1">
|
<div v-if="slot.planet" class="space-y-1">
|
||||||
<!-- 第一行:名称、坐标、状态、残骸 -->
|
<!-- 第一行:名称、坐标、状态、残骸 -->
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-wrap">
|
<div class="flex items-center gap-1.5 min-w-0 flex-wrap">
|
||||||
<h3 class="font-semibold text-sm truncate">{{ slot.planet.name }}</h3>
|
<h3 class="font-semibold text-sm truncate">
|
||||||
|
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||||
|
</h3>
|
||||||
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
||||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||||
</span>
|
</span>
|
||||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
|
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
|
||||||
{{ t('galaxyView.mine') }}
|
{{ t('galaxyView.mine') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0">
|
<Popover v-else>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0 cursor-pointer">
|
||||||
{{ getRelationStatusText(slot.planet) }}
|
{{ getRelationStatusText(slot.planet) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent v-if="getReputationValue(slot.planet) !== null" class="w-auto p-3" side="top" align="center">
|
||||||
|
<p class="text-sm">
|
||||||
|
{{ t('diplomacy.reputation') }}:
|
||||||
|
<span :class="getReputationColor(getReputationValue(slot.planet))" class="font-medium">
|
||||||
|
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -224,7 +238,7 @@
|
|||||||
<Recycle class="h-3 w-3" />
|
<Recycle class="h-3 w-3" />
|
||||||
</Badge>
|
</Badge>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
<PopoverContent class="w-auto p-3" side="top" align="center">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
||||||
<div class="space-y-1 text-xs">
|
<div class="space-y-1 text-xs">
|
||||||
@@ -247,13 +261,6 @@
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<!-- 第二行:好感度 -->
|
|
||||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
|
||||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
|
||||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
|
||||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 空位置 -->
|
<!-- 空位置 -->
|
||||||
<div v-else class="space-y-1">
|
<div v-else class="space-y-1">
|
||||||
@@ -389,11 +396,27 @@
|
|||||||
<div v-if="slot.planet" class="space-y-1">
|
<div v-if="slot.planet" class="space-y-1">
|
||||||
<!-- PC端:标题和徽章 -->
|
<!-- PC端:标题和徽章 -->
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<h3 class="font-semibold text-base">{{ slot.planet.name }}</h3>
|
<h3 class="font-semibold text-base">
|
||||||
|
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||||
|
</h3>
|
||||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
|
<TooltipProvider v-else :delay-duration="300">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs cursor-default">
|
||||||
{{ getRelationStatusText(slot.planet) }}
|
{{ getRelationStatusText(slot.planet) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent v-if="getReputationValue(slot.planet) !== null">
|
||||||
|
<p>
|
||||||
|
{{ t('diplomacy.reputation') }}:
|
||||||
|
<span :class="getReputationColor(getReputationValue(slot.planet))">
|
||||||
|
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
<!-- 残骸场徽章 -->
|
<!-- 残骸场徽章 -->
|
||||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
@@ -432,13 +455,6 @@
|
|||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||||
</p>
|
</p>
|
||||||
<!-- PC端:好感度显示(仅NPC星球) -->
|
|
||||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
|
||||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
|
||||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
|
||||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 空位置 -->
|
<!-- 空位置 -->
|
||||||
<div v-else class="space-y-1">
|
<div v-else class="space-y-1">
|
||||||
@@ -744,9 +760,6 @@
|
|||||||
selectedGalaxy.value = queryGalaxy
|
selectedGalaxy.value = queryGalaxy
|
||||||
selectedSystem.value = querySystem
|
selectedSystem.value = querySystem
|
||||||
loadSystem()
|
loadSystem()
|
||||||
|
|
||||||
// 立即清除URL参数,但保持本地变量中的highlightNpcId
|
|
||||||
clearUrlParams()
|
|
||||||
} else if (gameStore.currentPlanet) {
|
} else if (gameStore.currentPlanet) {
|
||||||
// 否则默认显示当前星球所在的星系
|
// 否则默认显示当前星球所在的星系
|
||||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||||
@@ -778,13 +791,6 @@
|
|||||||
return universeStore.debrisFields[debrisId] || null
|
return universeStore.debrisFields[debrisId] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除URL参数
|
|
||||||
const clearUrlParams = () => {
|
|
||||||
if (route.query.highlightNpc || route.query.galaxy || route.query.system) {
|
|
||||||
router.replace({ query: {} })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载星系
|
// 加载星系
|
||||||
const loadSystem = () => {
|
const loadSystem = () => {
|
||||||
currentGalaxy.value = selectedGalaxy.value
|
currentGalaxy.value = selectedGalaxy.value
|
||||||
@@ -833,7 +839,8 @@
|
|||||||
const getRelation = (planet: Planet | null) => {
|
const getRelation = (planet: Planet | null) => {
|
||||||
const npc = getPlanetNPC(planet)
|
const npc = getPlanetNPC(planet)
|
||||||
if (!npc) return null
|
if (!npc) return null
|
||||||
return gameStore.player.diplomaticRelations?.[npc.id]
|
// 从NPC的relations中获取对玩家的关系
|
||||||
|
return npc.relations?.[gameStore.player.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取关系状态Badge样式
|
// 获取关系状态Badge样式
|
||||||
@@ -880,6 +887,17 @@
|
|||||||
return 'text-muted-foreground'
|
return 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取NPC星球的显示名称 - 使用"XXX的星球"格式,如果有备注则显示"NPC名称(备注)的星球"
|
||||||
|
const getNpcPlanetDisplayName = (planet: Planet | null): string => {
|
||||||
|
if (!planet) return ''
|
||||||
|
const npc = getPlanetNPC(planet)
|
||||||
|
if (npc) {
|
||||||
|
const displayName = npc.note ? `${npc.name}(${npc.note})` : npc.name
|
||||||
|
return t('galaxyView.npcPlanetName').replace('{name}', displayName)
|
||||||
|
}
|
||||||
|
return planet.name
|
||||||
|
}
|
||||||
|
|
||||||
// 切换到指定星球
|
// 切换到指定星球
|
||||||
const switchToPlanet = (planetId: string) => {
|
const switchToPlanet = (planetId: string) => {
|
||||||
gameStore.currentPlanetId = planetId
|
gameStore.currentPlanetId = planetId
|
||||||
|
|||||||
159
src/views/HomeView.vue
Normal file
159
src/views/HomeView.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/50 p-4">
|
||||||
|
<!-- Logo 和标题 -->
|
||||||
|
<div class="text-center mb-8 animate-fade-in">
|
||||||
|
<img src="@/assets/logo.svg" alt="OGame Logo" class="w-24 h-24 mx-auto mb-4" />
|
||||||
|
<h1 class="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||||
|
{{ pkg.title }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm sm:text-base">{{ t('home.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 开始游戏按钮 -->
|
||||||
|
<div class="flex flex-col gap-4 w-full max-w-xs mb-8">
|
||||||
|
<Button size="lg" class="w-full text-lg h-14" @click="handleStartGame">
|
||||||
|
<Rocket class="mr-2 h-5 w-5" />
|
||||||
|
{{ t('home.startGame') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||||
|
<!-- 语言切换 -->
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Languages class="mr-2 h-4 w-4" />
|
||||||
|
{{ localeNames[gameStore.locale] }}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-48 p-2" side="top">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Button
|
||||||
|
v-for="locale in availableLocales"
|
||||||
|
:key="locale"
|
||||||
|
@click="gameStore.locale = locale"
|
||||||
|
:variant="gameStore.locale === locale ? 'secondary' : 'ghost'"
|
||||||
|
class="w-full justify-start"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ localeNames[locale] }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<!-- 隐私协议按钮 -->
|
||||||
|
<Button variant="ghost" size="sm" @click="showPrivacyDialog = true">
|
||||||
|
<Shield class="mr-2 h-4 w-4" />
|
||||||
|
{{ t('settings.privacyPolicy') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐私协议弹窗 -->
|
||||||
|
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||||
|
|
||||||
|
<!-- 隐私协议同意确认弹窗(开始游戏时) -->
|
||||||
|
<AlertDialog v-model:open="showAgreementDialog">
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{{ t('home.privacyAgreement') }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{{ t('home.privacyAgreementDesc') }}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<div class="my-4 max-h-48 overflow-y-auto text-sm text-muted-foreground border rounded-lg p-3">
|
||||||
|
<p>{{ t('privacy.sections.introduction.content') }}</p>
|
||||||
|
<p class="mt-2">{{ t('privacy.sections.dataCollection.content') }}</p>
|
||||||
|
<p class="mt-2">{{ t('privacy.sections.thirdParty.content') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<Checkbox id="privacy-agree" v-model="privacyAgreed" />
|
||||||
|
<label for="privacy-agree" class="text-sm cursor-pointer">
|
||||||
|
{{ t('home.agreeToPrivacy') }}
|
||||||
|
<Button variant="link" class="p-0 h-auto text-sm" @click.prevent="showPrivacyDialog = true">
|
||||||
|
{{ t('home.viewFullPolicy') }}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel @click="privacyAgreed = false">{{ t('common.cancel') }}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction :disabled="!privacyAgreed" @click="confirmStartGame">
|
||||||
|
{{ t('home.agreeAndStart') }}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { localeNames, type Locale } from '@/locales'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Rocket, Languages, Shield } from 'lucide-vue-next'
|
||||||
|
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||||
|
import pkg from '../../package.json'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const showPrivacyDialog = ref(false)
|
||||||
|
const showAgreementDialog = ref(false)
|
||||||
|
const privacyAgreed = ref(false)
|
||||||
|
|
||||||
|
const availableLocales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||||
|
|
||||||
|
// 处理开始游戏
|
||||||
|
const handleStartGame = () => {
|
||||||
|
// 如果已经同意过,直接进入总览页面
|
||||||
|
if (gameStore.player.privacyAgreed) {
|
||||||
|
router.push('/overview')
|
||||||
|
} else {
|
||||||
|
// 显示同意弹窗
|
||||||
|
showAgreementDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认开始游戏
|
||||||
|
const confirmStartGame = () => {
|
||||||
|
if (privacyAgreed.value) {
|
||||||
|
gameStore.player.privacyAgreed = true
|
||||||
|
showAgreementDialog.value = false
|
||||||
|
router.push('/overview')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,24 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||||
|
|
||||||
|
<!-- 清空消息按钮 -->
|
||||||
|
<Popover v-model:open="showClearPopover">
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm" class="gap-2">
|
||||||
|
<Trash2 class="h-4 w-4" />
|
||||||
|
{{ t('messagesView.clearMessages') }}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-80">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium leading-none">{{ t('messagesView.clearMessageTypes') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="option in clearOptionFields" :key="option.key" class="flex items-center space-x-2">
|
||||||
|
<Checkbox :id="`clear-${option.key}`" v-model="clearOptions[option.key]" />
|
||||||
|
<label :for="`clear-${option.key}`" class="text-sm cursor-pointer">
|
||||||
|
{{ t(`messagesView.${option.labelKey}`) }} ({{ option.count }})
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button @click="clearSelectedMessages" class="w-full" :disabled="!hasSelectedAny">
|
||||||
|
{{ t('messagesView.clearNow') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 标签切换 -->
|
<!-- 标签切换 -->
|
||||||
<Tabs v-model="activeTab" class="w-full">
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
<TabsList class="grid w-full grid-cols-2 sm:grid-cols-4" :tab-count="4">
|
||||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="hidden sm:flex ml-1">
|
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="ml-1 text-xs">
|
||||||
{{ tab.unreadCount }}
|
{{ tab.unreadCount }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- 战斗报告列表 -->
|
<!-- 战斗报告列表 -->
|
||||||
<TabsContent value="battles" class="mt-4 space-y-2">
|
<TabsContent value="battles" class="mt-4 space-y-2 pb-20">
|
||||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
<Empty v-if="gameStore.player.battleReports.length === 0" class="border rounded-lg">
|
||||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
<EmptyContent>
|
||||||
</Card>
|
<Sword class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('messagesView.noBattleReports') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
v-for="report in sortedBattleReports"
|
v-for="report in sortedBattleReports"
|
||||||
@@ -48,10 +81,13 @@
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
||||||
<TabsContent value="spy" class="mt-4 space-y-2">
|
<TabsContent value="spy" class="mt-4 space-y-2 pb-20">
|
||||||
<Card v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0">
|
<Empty v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0" class="border rounded-lg">
|
||||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
<EmptyContent>
|
||||||
</Card>
|
<Eye class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('messagesView.noSpyReports') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
|
||||||
<!-- 侦查报告 -->
|
<!-- 侦查报告 -->
|
||||||
<Card
|
<Card
|
||||||
@@ -107,16 +143,20 @@
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
||||||
<TabsContent value="npc" class="mt-4 space-y-2">
|
<TabsContent value="npc" class="mt-4 space-y-2 pb-20">
|
||||||
<Card
|
<Empty
|
||||||
v-if="
|
v-if="
|
||||||
sortedNPCActivityNotifications.length === 0 &&
|
sortedNPCActivityNotifications.length === 0 &&
|
||||||
sortedGiftNotifications.length === 0 &&
|
sortedGiftNotifications.length === 0 &&
|
||||||
sortedGiftRejectedNotifications.length === 0
|
sortedGiftRejectedNotifications.length === 0
|
||||||
"
|
"
|
||||||
|
class="border rounded-lg"
|
||||||
>
|
>
|
||||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noNPCActivity') }}</CardContent>
|
<EmptyContent>
|
||||||
</Card>
|
<Users class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('messagesView.noNPCActivity') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
|
||||||
<!-- NPC活动通知 -->
|
<!-- NPC活动通知 -->
|
||||||
<Card
|
<Card
|
||||||
@@ -177,11 +217,11 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div v-if="gift.resources.metal > 0">{{ t('resources.metal') }}: {{ gift.resources.metal.toLocaleString() }}</div>
|
<template v-for="res in basicResourceFields" :key="res.key">
|
||||||
<div v-if="gift.resources.crystal > 0">{{ t('resources.crystal') }}: {{ gift.resources.crystal.toLocaleString() }}</div>
|
<div v-if="gift.resources[res.key] > 0">
|
||||||
<div v-if="gift.resources.deuterium > 0">
|
{{ t(`resources.${res.key}`) }}: {{ gift.resources[res.key].toLocaleString() }}
|
||||||
{{ t('resources.deuterium') }}: {{ gift.resources.deuterium.toLocaleString() }}
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
@@ -229,15 +269,11 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div v-if="rejection.rejectedResources.metal > 0">
|
<template v-for="res in basicResourceFields" :key="res.key">
|
||||||
{{ t('resources.metal') }}: {{ rejection.rejectedResources.metal.toLocaleString() }}
|
<div v-if="rejection.rejectedResources[res.key] > 0">
|
||||||
</div>
|
{{ t(`resources.${res.key}`) }}: {{ rejection.rejectedResources[res.key].toLocaleString() }}
|
||||||
<div v-if="rejection.rejectedResources.crystal > 0">
|
|
||||||
{{ t('resources.crystal') }}: {{ rejection.rejectedResources.crystal.toLocaleString() }}
|
|
||||||
</div>
|
|
||||||
<div v-if="rejection.rejectedResources.deuterium > 0">
|
|
||||||
{{ t('resources.deuterium') }}: {{ rejection.rejectedResources.deuterium.toLocaleString() }}
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
@@ -253,10 +289,13 @@
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 任务报告列表 -->
|
<!-- 任务报告列表 -->
|
||||||
<TabsContent value="missions" class="mt-4 space-y-2">
|
<TabsContent value="missions" class="mt-4 space-y-2 pb-20">
|
||||||
<Card v-if="sortedMissionReports.length === 0">
|
<Empty v-if="sortedMissionReports.length === 0" class="border rounded-lg">
|
||||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noMissionReports') }}</CardContent>
|
<EmptyContent>
|
||||||
</Card>
|
<Package class="h-10 w-10 text-muted-foreground" />
|
||||||
|
<EmptyDescription>{{ t('messagesView.noMissionReports') }}</EmptyDescription>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
v-for="report in sortedMissionReports"
|
v-for="report in sortedMissionReports"
|
||||||
@@ -291,11 +330,331 @@
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
<!-- 固定底部分页 -->
|
||||||
|
<FixedPagination v-model:page="currentPage[activeTab]" :total-pages="getPaginationConfig(activeTab).totalPages" />
|
||||||
|
|
||||||
<!-- 战斗报告对话框 -->
|
<!-- 战斗报告对话框 -->
|
||||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||||
|
|
||||||
<!-- 间谍报告对话框 -->
|
<!-- 间谍报告对话框 -->
|
||||||
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
|
<SpyReportDialog v-model:open="showSpyDialog" :report="selectedSpyReport" />
|
||||||
|
|
||||||
|
<!-- 被侦查通知详情对话框 -->
|
||||||
|
<Dialog :open="showSpiedDialog" @update:open="showSpiedDialog = $event">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Eye class="h-5 w-5 text-purple-500" />
|
||||||
|
{{ t('messagesView.spiedNotificationDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('messagesView.spyDetected') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="selectedSpiedNotification" class="space-y-4">
|
||||||
|
<!-- 侦查者信息 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{{ selectedSpiedNotification.npcName }}</h3>
|
||||||
|
<Badge variant="destructive">{{ t('messagesView.spyDetected') }}</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatDate(selectedSpiedNotification.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 被侦查星球 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.targetPlanet') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md flex items-center gap-2">
|
||||||
|
<Globe class="h-4 w-4 text-blue-500" />
|
||||||
|
<span class="font-medium">{{ selectedSpiedNotification.targetPlanetName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 检测结果 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.detectionResult') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div v-if="selectedSpiedNotification.detectionSuccess" class="flex items-center gap-2 text-yellow-600 dark:text-yellow-400">
|
||||||
|
<AlertTriangle class="h-5 w-5" />
|
||||||
|
<span class="font-medium">{{ t('messagesView.detectionSuccess') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mt-2">
|
||||||
|
{{
|
||||||
|
t('messagesView.spiedNotificationMessage', {
|
||||||
|
npc: selectedSpiedNotification.npcName,
|
||||||
|
planet: selectedSpiedNotification.targetPlanetName
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 建议 -->
|
||||||
|
<div class="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
|
||||||
|
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
{{ t('messagesView.spiedNotificationTip') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showSpiedDialog = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="viewNPCInGalaxy(selectedSpiedNotification?.npcId)">{{ t('messagesView.viewInGalaxy') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- 任务报告详情对话框 -->
|
||||||
|
<Dialog :open="showMissionDialog" @update:open="showMissionDialog = $event">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
|
||||||
|
{{ t('messagesView.missionReportDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('messagesView.missionDetails') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="selectedMissionReport" class="space-y-4">
|
||||||
|
<!-- 任务状态 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{{ getMissionTypeName(selectedMissionReport.missionType) }}</h3>
|
||||||
|
<Badge :variant="selectedMissionReport.success ? 'default' : 'destructive'">
|
||||||
|
{{ selectedMissionReport.success ? t('messagesView.missionSuccess') : t('messagesView.missionFailed') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatDate(selectedMissionReport.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 起点和终点 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.origin') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium">{{ selectedMissionReport.originPlanetName }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.destination') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium" v-if="selectedMissionReport.targetPlanetName">{{ selectedMissionReport.targetPlanetName }}</p>
|
||||||
|
<p class="text-sm text-muted-foreground" v-else>
|
||||||
|
[{{ selectedMissionReport.targetPosition.galaxy }}:{{ selectedMissionReport.targetPosition.system }}:{{
|
||||||
|
selectedMissionReport.targetPosition.position
|
||||||
|
}}]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务详情 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.missionDetails') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="text-sm mb-2">{{ selectedMissionReport.message }}</p>
|
||||||
|
|
||||||
|
<!-- 运输任务详情 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.transportedResources" class="mt-3 space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.transportedResources') }}:</p>
|
||||||
|
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||||
|
<div v-for="res in basicResourceFields" :key="res.key">
|
||||||
|
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 回收任务详情 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.recycledResources" class="mt-3 space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.recycledResources') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||||
|
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMissionReport.details.remainingDebris" class="mt-2">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.remainingDebris') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||||
|
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 殖民任务详情 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.newPlanetName" class="mt-3">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.newPlanet') }}:</p>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<Globe class="h-4 w-4 text-green-500" />
|
||||||
|
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 导弹攻击详情 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.missileCount !== undefined" class="mt-3 space-y-2">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.missileAttack') }}:</p>
|
||||||
|
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground">{{ t('galaxyView.missileCount') }}:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ selectedMissionReport.details.missileCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground">{{ t('missionReports.hits') }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-green-600">{{ selectedMissionReport.details.missileHits }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground">{{ t('galaxyView.intercepted') }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-yellow-600">{{ selectedMissionReport.details.missileIntercepted }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="Object.keys(selectedMissionReport.details.defenseLosses || {}).length > 0" class="mt-2">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.defenseLosses') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-1 text-xs mt-1 p-2 bg-red-50 dark:bg-red-950/30 rounded">
|
||||||
|
<div v-for="(count, defenseType) in selectedMissionReport.details.defenseLosses" :key="defenseType">
|
||||||
|
<span class="text-muted-foreground">{{ t('defenses.' + defenseType) }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 发现资源 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.foundResources" class="mt-3 space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.resources') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
||||||
|
<div v-for="res in allResourceFields" :key="res.key">
|
||||||
|
<template v-if="(selectedMissionReport.details?.foundResources?.[res.key] ?? 0) > 0">
|
||||||
|
<span class="text-muted-foreground">{{ t(`resources.${res.key}`) }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-green-600 dark:text-green-400">
|
||||||
|
+{{ (selectedMissionReport.details?.foundResources?.[res.key] ?? 0).toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 发现舰船 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.foundFleet" class="mt-3 space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.fleet') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
||||||
|
<div v-for="(count, shipType) in selectedMissionReport.details.foundFleet" :key="shipType">
|
||||||
|
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-blue-600 dark:text-blue-400">+{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 探险任务详情 - 损失舰船 -->
|
||||||
|
<div v-if="selectedMissionReport.details?.fleetLost" class="mt-3 space-y-1">
|
||||||
|
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.attackerLosses') }}:</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm p-2 bg-red-50 dark:bg-red-950/30 rounded">
|
||||||
|
<div v-for="(count, shipType) in selectedMissionReport.details.fleetLost" :key="shipType">
|
||||||
|
<span class="text-muted-foreground">{{ t('ships.' + shipType) }}:</span>
|
||||||
|
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showMissionDialog = false">{{ t('common.close') }}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- NPC活动通知详情对话框 -->
|
||||||
|
<Dialog :open="showNPCActivityDialog" @update:open="showNPCActivityDialog = $event">
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle class="flex items-center gap-2">
|
||||||
|
<Recycle class="h-5 w-5 text-yellow-500" />
|
||||||
|
{{ t('messagesView.npcActivityDetails') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('messagesView.activityDescription') }}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="selectedNPCActivityNotification" class="space-y-4">
|
||||||
|
<!-- NPC信息 -->
|
||||||
|
<div class="p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="font-semibold text-lg">{{ selectedNPCActivityNotification.npcName }}</h3>
|
||||||
|
<Badge variant="secondary">{{ t('messagesView.activityType.' + selectedNPCActivityNotification.activityType) }}</Badge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ formatDate(selectedNPCActivityNotification.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活动位置 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.activityLocation') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Globe class="h-4 w-4 text-blue-500" />
|
||||||
|
<span class="font-medium">
|
||||||
|
{{ t('messagesView.position') }}: [{{ selectedNPCActivityNotification.targetPosition.galaxy }}:{{
|
||||||
|
selectedNPCActivityNotification.targetPosition.system
|
||||||
|
}}:{{ selectedNPCActivityNotification.targetPosition.position }}]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="selectedNPCActivityNotification.targetPlanetName" class="text-sm text-muted-foreground">
|
||||||
|
{{ t('messagesView.nearPlanet') }}: {{ selectedNPCActivityNotification.targetPlanetName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活动描述 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.activityDescription') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="text-sm">
|
||||||
|
{{
|
||||||
|
t('messagesView.npcActivityMessage', {
|
||||||
|
npc: selectedNPCActivityNotification.npcName,
|
||||||
|
activity: t('messagesView.activityType.' + selectedNPCActivityNotification.activityType),
|
||||||
|
position: `[${selectedNPCActivityNotification.targetPosition.galaxy}:${selectedNPCActivityNotification.targetPosition.system}:${selectedNPCActivityNotification.targetPosition.position}]`
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 到达时间 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-semibold text-sm">{{ t('messagesView.arrivalTime') }}</h4>
|
||||||
|
<div class="p-3 bg-muted/30 rounded-md">
|
||||||
|
<p class="font-medium">{{ formatDate(selectedNPCActivityNotification.arrivalTime) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<div class="p-3 bg-yellow-50 dark:bg-yellow-950/30 rounded-md border border-yellow-200 dark:border-yellow-800">
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
|
{{ t('messagesView.npcActivityTip') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="showNPCActivityDialog = false">{{ t('common.close') }}</Button>
|
||||||
|
<Button @click="viewLocationInGalaxy(selectedNPCActivityNotification?.targetPosition)">
|
||||||
|
{{ t('messagesView.viewInGalaxy') }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -303,14 +662,20 @@
|
|||||||
import { useGameStore } from '@/stores/gameStore'
|
import { useGameStore } from '@/stores/gameStore'
|
||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
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'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { FixedPagination } from '@/components/ui/pagination'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||||
import { formatDate } from '@/utils/format'
|
import { formatDate } from '@/utils/format'
|
||||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users } from 'lucide-vue-next'
|
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||||
import type {
|
import type {
|
||||||
BattleResult,
|
BattleResult,
|
||||||
SpyReport,
|
SpyReport,
|
||||||
@@ -323,52 +688,232 @@
|
|||||||
import { MissionType } from '@/types/game'
|
import { MissionType } from '@/types/game'
|
||||||
import { useNPCStore } from '@/stores/npcStore'
|
import { useNPCStore } from '@/stores/npcStore'
|
||||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
||||||
|
|
||||||
|
// 清空消息功能
|
||||||
|
const showClearPopover = ref(false)
|
||||||
|
type ClearOptionKey =
|
||||||
|
| 'battles'
|
||||||
|
| 'spyReports'
|
||||||
|
| 'spiedNotifications'
|
||||||
|
| 'missionReports'
|
||||||
|
| 'npcActivity'
|
||||||
|
| 'giftNotifications'
|
||||||
|
| 'giftRejected'
|
||||||
|
const clearOptions = ref<Record<ClearOptionKey, boolean>>({
|
||||||
|
battles: false,
|
||||||
|
spyReports: false,
|
||||||
|
spiedNotifications: false,
|
||||||
|
missionReports: false,
|
||||||
|
npcActivity: false,
|
||||||
|
giftNotifications: false,
|
||||||
|
giftRejected: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空消息选项配置
|
||||||
|
const clearOptionFields = computed(() => [
|
||||||
|
{ key: 'battles' as ClearOptionKey, labelKey: 'clearBattleReports', count: gameStore.player.battleReports.length },
|
||||||
|
{ key: 'spyReports' as ClearOptionKey, labelKey: 'clearSpyReports', count: gameStore.player.spyReports.length },
|
||||||
|
{
|
||||||
|
key: 'spiedNotifications' as ClearOptionKey,
|
||||||
|
labelKey: 'clearSpiedNotifications',
|
||||||
|
count: gameStore.player.spiedNotifications?.length || 0
|
||||||
|
},
|
||||||
|
{ key: 'missionReports' as ClearOptionKey, labelKey: 'clearMissionReports', count: gameStore.player.missionReports?.length || 0 },
|
||||||
|
{ key: 'npcActivity' as ClearOptionKey, labelKey: 'clearNPCActivity', count: gameStore.player.npcActivityNotifications?.length || 0 },
|
||||||
|
{
|
||||||
|
key: 'giftNotifications' as ClearOptionKey,
|
||||||
|
labelKey: 'clearGiftNotifications',
|
||||||
|
count: gameStore.player.giftNotifications?.length || 0
|
||||||
|
},
|
||||||
|
{ key: 'giftRejected' as ClearOptionKey, labelKey: 'clearGiftRejected', count: gameStore.player.giftRejectedNotifications?.length || 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 基础资源字段配置(用于显示资源列表)
|
||||||
|
type BasicResourceKey = 'metal' | 'crystal' | 'deuterium'
|
||||||
|
const basicResourceFields: { key: BasicResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }]
|
||||||
|
|
||||||
|
// 残骸资源字段配置(只有金属和晶体)
|
||||||
|
type DebrisResourceKey = 'metal' | 'crystal'
|
||||||
|
const debrisResourceFields: { key: DebrisResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }]
|
||||||
|
|
||||||
|
// 全部资源字段配置(包含暗物质,用于探险任务)
|
||||||
|
type AllResourceKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||||
|
const allResourceFields: { key: AllResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
|
||||||
|
|
||||||
|
const hasSelectedAny = computed(() => {
|
||||||
|
return Object.values(clearOptions.value).some(v => v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const clearSelectedMessages = () => {
|
||||||
|
if (clearOptions.value.battles) {
|
||||||
|
gameStore.player.battleReports = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.spyReports) {
|
||||||
|
gameStore.player.spyReports = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.spiedNotifications) {
|
||||||
|
gameStore.player.spiedNotifications = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.missionReports) {
|
||||||
|
gameStore.player.missionReports = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.npcActivity) {
|
||||||
|
gameStore.player.npcActivityNotifications = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.giftNotifications) {
|
||||||
|
gameStore.player.giftNotifications = []
|
||||||
|
}
|
||||||
|
if (clearOptions.value.giftRejected) {
|
||||||
|
gameStore.player.giftRejectedNotifications = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置选项
|
||||||
|
clearOptions.value = {
|
||||||
|
battles: false,
|
||||||
|
spyReports: false,
|
||||||
|
spiedNotifications: false,
|
||||||
|
missionReports: false,
|
||||||
|
npcActivity: false,
|
||||||
|
giftNotifications: false,
|
||||||
|
giftRejected: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭popover
|
||||||
|
showClearPopover.value = false
|
||||||
|
|
||||||
|
// 显示成功提示
|
||||||
|
toast.success(t('messagesView.clearSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
const ITEMS_PER_PAGE = 10
|
||||||
|
const currentPage = ref({
|
||||||
|
battles: 1,
|
||||||
|
spy: 1,
|
||||||
|
missions: 1,
|
||||||
|
npc: 1
|
||||||
|
})
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
const showBattleDialog = ref(false)
|
const showBattleDialog = ref(false)
|
||||||
const showSpyDialog = ref(false)
|
const showSpyDialog = ref(false)
|
||||||
|
const showSpiedDialog = ref(false)
|
||||||
|
const showMissionDialog = ref(false)
|
||||||
|
const showNPCActivityDialog = ref(false)
|
||||||
const selectedBattleReport = ref<BattleResult | null>(null)
|
const selectedBattleReport = ref<BattleResult | null>(null)
|
||||||
const selectedSpyReport = ref<SpyReport | null>(null)
|
const selectedSpyReport = ref<SpyReport | null>(null)
|
||||||
|
const selectedSpiedNotification = ref<SpiedNotification | null>(null)
|
||||||
|
const selectedMissionReport = ref<MissionReport | null>(null)
|
||||||
|
const selectedNPCActivityNotification = ref<NPCActivityNotification | null>(null)
|
||||||
|
|
||||||
// 排序后的战斗报告(最新的在前)
|
// 排序后的战斗报告(最新的在前)- 全部数据
|
||||||
const sortedBattleReports = computed(() => {
|
const allBattleReports = computed(() => {
|
||||||
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 排序后的间谍报告(最新的在前)
|
// 排序后的间谍报告(最新的在前)- 全部数据
|
||||||
const sortedSpyReports = computed(() => {
|
const allSpyReports = computed(() => {
|
||||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 排序后的被侦查通知(最新的在前)
|
// 排序后的被侦查通知(最新的在前)- 全部数据
|
||||||
const sortedSpiedNotifications = computed(() => {
|
const allSpiedNotifications = computed(() => {
|
||||||
if (!gameStore.player.spiedNotifications) {
|
if (!gameStore.player.spiedNotifications) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 排序后的任务报告(最新的在前)
|
// 排序后的任务报告(最新的在前)- 全部数据
|
||||||
const sortedMissionReports = computed(() => {
|
const allMissionReports = computed(() => {
|
||||||
if (!gameStore.player.missionReports) {
|
if (!gameStore.player.missionReports) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 排序后的NPC活动通知(最新的在前)
|
// 排序后的NPC活动通知(最新的在前)- 全部数据
|
||||||
const sortedNPCActivityNotifications = computed(() => {
|
const allNPCActivityNotifications = computed(() => {
|
||||||
if (!gameStore.player.npcActivityNotifications) {
|
if (!gameStore.player.npcActivityNotifications) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 战斗报告分页
|
||||||
|
const battleReportsTotalPages = computed(() => Math.ceil(allBattleReports.value.length / ITEMS_PER_PAGE))
|
||||||
|
const sortedBattleReports = computed(() => {
|
||||||
|
const start = (currentPage.value.battles - 1) * ITEMS_PER_PAGE
|
||||||
|
const end = start + ITEMS_PER_PAGE
|
||||||
|
return allBattleReports.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 侦查标签页合并数据(侦查报告 + 被侦查通知)
|
||||||
|
const allSpyTabItems = computed(() => {
|
||||||
|
const spyReports = allSpyReports.value.map(item => ({ ...item, type: 'spy' as const }))
|
||||||
|
const spiedNotifications = allSpiedNotifications.value.map(item => ({ ...item, type: 'spied' as const }))
|
||||||
|
return [...spyReports, ...spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
const spyTabTotalPages = computed(() => Math.ceil(allSpyTabItems.value.length / ITEMS_PER_PAGE))
|
||||||
|
const paginatedSpyTabItems = computed(() => {
|
||||||
|
const start = (currentPage.value.spy - 1) * ITEMS_PER_PAGE
|
||||||
|
const end = start + ITEMS_PER_PAGE
|
||||||
|
return allSpyTabItems.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从分页后的混合数据中分离出侦查报告和被侦查通知
|
||||||
|
const sortedSpyReports = computed(() => {
|
||||||
|
return paginatedSpyTabItems.value.filter(item => item.type === 'spy')
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedSpiedNotifications = computed(() => {
|
||||||
|
return paginatedSpyTabItems.value.filter(item => item.type === 'spied')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 任务报告分页
|
||||||
|
const missionReportsTotalPages = computed(() => Math.ceil(allMissionReports.value.length / ITEMS_PER_PAGE))
|
||||||
|
const sortedMissionReports = computed(() => {
|
||||||
|
const start = (currentPage.value.missions - 1) * ITEMS_PER_PAGE
|
||||||
|
const end = start + ITEMS_PER_PAGE
|
||||||
|
return allMissionReports.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// NPC标签页合并数据(活动通知 + 礼物通知 + 礼物被拒绝通知)
|
||||||
|
const allNPCTabItems = computed(() => {
|
||||||
|
const activities = allNPCActivityNotifications.value.map(item => ({ ...item, type: 'activity' as const }))
|
||||||
|
const gifts = allGiftNotifications.value.map(item => ({ ...item, type: 'gift' as const }))
|
||||||
|
const rejections = allGiftRejectedNotifications.value.map(item => ({ ...item, type: 'rejection' as const }))
|
||||||
|
return [...activities, ...gifts, ...rejections].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
|
const npcTabTotalPages = computed(() => Math.ceil(allNPCTabItems.value.length / ITEMS_PER_PAGE))
|
||||||
|
const paginatedNPCTabItems = computed(() => {
|
||||||
|
const start = (currentPage.value.npc - 1) * ITEMS_PER_PAGE
|
||||||
|
const end = start + ITEMS_PER_PAGE
|
||||||
|
return allNPCTabItems.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从分页后的混合数据中分离出各种NPC消息
|
||||||
|
const sortedNPCActivityNotifications = computed(() => {
|
||||||
|
return paginatedNPCTabItems.value.filter(item => item.type === 'activity')
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedGiftNotifications = computed(() => {
|
||||||
|
return paginatedNPCTabItems.value.filter(item => item.type === 'gift')
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedGiftRejectedNotifications = computed(() => {
|
||||||
|
return paginatedNPCTabItems.value.filter(item => item.type === 'rejection')
|
||||||
|
})
|
||||||
|
|
||||||
// 未读战斗报告数量
|
// 未读战斗报告数量
|
||||||
const unreadBattles = computed(() => {
|
const unreadBattles = computed(() => {
|
||||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||||
@@ -457,22 +1002,41 @@
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
// 排序后的礼物通知(最新的在前)
|
// 礼物通知和被拒绝通知的全部数据(用于NPC标签页合并)
|
||||||
const sortedGiftNotifications = computed(() => {
|
const allGiftNotifications = computed(() => {
|
||||||
if (!gameStore.player.giftNotifications) {
|
if (!gameStore.player.giftNotifications) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 排序后的礼物被拒绝通知(最新的在前)
|
const allGiftRejectedNotifications = computed(() => {
|
||||||
const sortedGiftRejectedNotifications = computed(() => {
|
|
||||||
if (!gameStore.player.giftRejectedNotifications) {
|
if (!gameStore.player.giftRejectedNotifications) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
type PageKey = 'battles' | 'spy' | 'missions' | 'npc'
|
||||||
|
const paginationConfigs = computed(() => ({
|
||||||
|
battles: {
|
||||||
|
totalPages: battleReportsTotalPages.value
|
||||||
|
},
|
||||||
|
spy: {
|
||||||
|
totalPages: spyTabTotalPages.value
|
||||||
|
},
|
||||||
|
missions: {
|
||||||
|
totalPages: missionReportsTotalPages.value
|
||||||
|
},
|
||||||
|
npc: {
|
||||||
|
totalPages: npcTabTotalPages.value
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 获取指定标签页的分页配置
|
||||||
|
const getPaginationConfig = (key: PageKey) => paginationConfigs.value[key]
|
||||||
|
|
||||||
// 判断战斗结果Badge颜色
|
// 判断战斗结果Badge颜色
|
||||||
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
||||||
if (report.winner === 'draw') {
|
if (report.winner === 'draw') {
|
||||||
@@ -513,18 +1077,23 @@
|
|||||||
const openSpyReport = (report: SpyReport) => {
|
const openSpyReport = (report: SpyReport) => {
|
||||||
selectedSpyReport.value = report
|
selectedSpyReport.value = report
|
||||||
showSpyDialog.value = true
|
showSpyDialog.value = true
|
||||||
// 标记为已读
|
// 找到原始间谍报告对象并标记为已读(因为sortedSpyReports是副本)
|
||||||
if (!report.read) {
|
const originalReport = gameStore.player.spyReports.find(r => r.id === report.id)
|
||||||
report.read = true
|
if (originalReport && !originalReport.read) {
|
||||||
|
originalReport.read = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开被侦查通知
|
// 打开被侦查通知
|
||||||
const openSpiedNotification = (notification: SpiedNotification) => {
|
const openSpiedNotification = (notification: SpiedNotification) => {
|
||||||
// 标记为已读
|
// 找到原始通知对象并标记为已读(因为sortedSpiedNotifications是副本)
|
||||||
if (!notification.read) {
|
const originalNotification = gameStore.player.spiedNotifications?.find(n => n.id === notification.id)
|
||||||
notification.read = true
|
if (originalNotification && !originalNotification.read) {
|
||||||
|
originalNotification.read = true
|
||||||
}
|
}
|
||||||
|
// 设置选中的通知并打开详情对话框
|
||||||
|
selectedSpiedNotification.value = notification
|
||||||
|
showSpiedDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除战斗报告
|
// 删除战斗报告
|
||||||
@@ -556,10 +1125,14 @@
|
|||||||
|
|
||||||
// 打开NPC活动通知
|
// 打开NPC活动通知
|
||||||
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
||||||
// 标记为已读
|
// 找到原始通知对象并标记为已读(因为sortedNPCActivityNotifications是副本)
|
||||||
if (!notification.read) {
|
const originalNotification = gameStore.player.npcActivityNotifications?.find(n => n.id === notification.id)
|
||||||
notification.read = true
|
if (originalNotification && !originalNotification.read) {
|
||||||
|
originalNotification.read = true
|
||||||
}
|
}
|
||||||
|
// 设置选中的通知并打开详情对话框
|
||||||
|
selectedNPCActivityNotification.value = notification
|
||||||
|
showNPCActivityDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除NPC活动通知
|
// 删除NPC活动通知
|
||||||
@@ -579,6 +1152,7 @@
|
|||||||
[MissionType.Transport]: t('fleetView.transport'),
|
[MissionType.Transport]: t('fleetView.transport'),
|
||||||
[MissionType.Colonize]: t('fleetView.colonize'),
|
[MissionType.Colonize]: t('fleetView.colonize'),
|
||||||
[MissionType.Deploy]: t('fleetView.deploy'),
|
[MissionType.Deploy]: t('fleetView.deploy'),
|
||||||
|
[MissionType.Expedition]: t('fleetView.expedition'),
|
||||||
[MissionType.Recycle]: t('fleetView.recycle'),
|
[MissionType.Recycle]: t('fleetView.recycle'),
|
||||||
[MissionType.Destroy]: t('fleetView.destroy'),
|
[MissionType.Destroy]: t('fleetView.destroy'),
|
||||||
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
[MissionType.MissileAttack]: t('galaxyView.missileAttack')
|
||||||
@@ -592,6 +1166,9 @@
|
|||||||
if (!report.read) {
|
if (!report.read) {
|
||||||
report.read = true
|
report.read = true
|
||||||
}
|
}
|
||||||
|
// 设置选中的报告并打开详情对话框
|
||||||
|
selectedMissionReport.value = report
|
||||||
|
showMissionDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除任务报告
|
// 删除任务报告
|
||||||
@@ -607,8 +1184,10 @@
|
|||||||
|
|
||||||
// 标记礼物通知为已读
|
// 标记礼物通知为已读
|
||||||
const markGiftAsRead = (gift: GiftNotification) => {
|
const markGiftAsRead = (gift: GiftNotification) => {
|
||||||
if (!gift.read) {
|
// 找到原始礼物通知对象并标记为已读(因为gifts是副本)
|
||||||
gift.read = true
|
const originalGift = gameStore.player.giftNotifications?.find(g => g.id === gift.id)
|
||||||
|
if (originalGift && !originalGift.read) {
|
||||||
|
originalGift.read = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,8 +1220,10 @@
|
|||||||
|
|
||||||
// 标记礼物被拒绝通知为已读
|
// 标记礼物被拒绝通知为已读
|
||||||
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
||||||
if (!rejection.read) {
|
// 找到原始拒绝通知对象并标记为已读(因为rejections是副本)
|
||||||
rejection.read = true
|
const originalRejection = gameStore.player.giftRejectedNotifications?.find(r => r.id === rejection.id)
|
||||||
|
if (originalRejection && !originalRejection.read) {
|
||||||
|
originalRejection.read = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,4 +1237,58 @@
|
|||||||
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
gameStore.player.giftRejectedNotifications.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查看NPC在星系中的位置
|
||||||
|
const viewNPCInGalaxy = (npcId?: string) => {
|
||||||
|
if (!npcId) return
|
||||||
|
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||||
|
if (!npc || npc.planets.length === 0) return
|
||||||
|
|
||||||
|
const targetPlanet = npc.planets[0]
|
||||||
|
if (!targetPlanet) return
|
||||||
|
|
||||||
|
showSpiedDialog.value = false
|
||||||
|
router.push({
|
||||||
|
path: '/galaxy',
|
||||||
|
query: {
|
||||||
|
galaxy: targetPlanet.position.galaxy,
|
||||||
|
system: targetPlanet.position.system,
|
||||||
|
highlightNpc: npcId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看位置在星系中
|
||||||
|
const viewLocationInGalaxy = (position?: { galaxy: number; system: number; position: number }) => {
|
||||||
|
if (!position) return
|
||||||
|
|
||||||
|
showNPCActivityDialog.value = false
|
||||||
|
router.push({
|
||||||
|
path: '/galaxy',
|
||||||
|
query: {
|
||||||
|
galaxy: position.galaxy,
|
||||||
|
system: position.system
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务类型图标
|
||||||
|
const getMissionIcon = (missionType?: MissionType) => {
|
||||||
|
if (!missionType) return Package
|
||||||
|
|
||||||
|
switch (missionType) {
|
||||||
|
case MissionType.Transport:
|
||||||
|
return Package
|
||||||
|
case MissionType.Recycle:
|
||||||
|
return Recycle
|
||||||
|
case MissionType.Colonize:
|
||||||
|
return Globe
|
||||||
|
case MissionType.Expedition:
|
||||||
|
return Compass
|
||||||
|
case MissionType.Destroy:
|
||||||
|
return Skull
|
||||||
|
default:
|
||||||
|
return Package
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -201,6 +201,7 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import ResourceIcon from '@/components/ResourceIcon.vue'
|
import ResourceIcon from '@/components/ResourceIcon.vue'
|
||||||
import { formatNumber, getResourceColor } from '@/utils/format'
|
import { formatNumber, getResourceColor } from '@/utils/format'
|
||||||
|
import { scaleNumber } from '@/utils/speed'
|
||||||
import type { Planet } from '@/types/game'
|
import type { Planet } from '@/types/game'
|
||||||
import * as publicLogic from '@/logic/publicLogic'
|
import * as publicLogic from '@/logic/publicLogic'
|
||||||
import * as resourceLogic from '@/logic/resourceLogic'
|
import * as resourceLogic from '@/logic/resourceLogic'
|
||||||
@@ -209,25 +210,27 @@
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { SHIPS } = useGameConfig()
|
const { SHIPS } = useGameConfig()
|
||||||
const planet = computed(() => gameStore.currentPlanet)
|
const planet = computed(() => gameStore.currentPlanet)
|
||||||
const production = computed(() => (planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers) : null))
|
const production = computed(() =>
|
||||||
|
planet.value ? publicLogic.getResourceProduction(planet.value, gameStore.player.officers, gameStore.gameSpeed) : null
|
||||||
|
)
|
||||||
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
|
const capacity = computed(() => (planet.value ? publicLogic.getResourceCapacity(planet.value, gameStore.player.officers) : null))
|
||||||
|
|
||||||
// 能量消耗
|
// 能量消耗
|
||||||
const energyConsumption = computed(() => {
|
const energyConsumption = computed(() => {
|
||||||
if (!planet.value) return 0
|
if (!planet.value) return 0
|
||||||
return resourceLogic.calculateEnergyConsumption(planet.value)
|
return scaleNumber(resourceLogic.calculateEnergyConsumption(planet.value), gameStore.gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 资源产量详细breakdown
|
// 资源产量详细breakdown
|
||||||
const productionBreakdown = computed(() => {
|
const productionBreakdown = computed(() => {
|
||||||
if (!planet.value) return null
|
if (!planet.value) return null
|
||||||
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now())
|
return resourceLogic.calculateProductionBreakdown(planet.value, gameStore.player.officers, Date.now(), gameStore.gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 资源消耗详细breakdown
|
// 资源消耗详细breakdown
|
||||||
const consumptionBreakdown = computed(() => {
|
const consumptionBreakdown = computed(() => {
|
||||||
if (!planet.value) return null
|
if (!planet.value) return null
|
||||||
return resourceLogic.calculateConsumptionBreakdown(planet.value)
|
return resourceLogic.calculateConsumptionBreakdown(planet.value, gameStore.gameSpeed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 资源类型配置
|
// 资源类型配置
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
||||||
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" class="relative">
|
<Card v-for="techType in Object.values(TechnologyType)" :key="techType" :data-tech="techType" class="relative">
|
||||||
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
<CardUnlockOverlay :requirements="TECHNOLOGIES[techType].requirements" :currentLevel="getTechLevel(techType)" />
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -65,9 +65,18 @@
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
<AlertDialogTitle>{{ alertDialogTitle }}</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="whitespace-pre-line">
|
<AlertDialogDescription v-if="!alertDialogShowRequirements" class="whitespace-pre-line">
|
||||||
{{ alertDialogMessage }}
|
{{ alertDialogMessage }}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogDescription v-else>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-for="(req, index) in alertDialogRequirements" :key="index" class="flex items-center gap-2 text-sm">
|
||||||
|
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
|
||||||
|
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
|
||||||
|
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
<AlertDialogAction>{{ t('common.confirm') }}</AlertDialogAction>
|
||||||
@@ -98,8 +107,8 @@
|
|||||||
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 { 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'
|
||||||
@@ -116,6 +125,8 @@
|
|||||||
const alertDialogOpen = ref(false)
|
const alertDialogOpen = ref(false)
|
||||||
const alertDialogTitle = ref('')
|
const alertDialogTitle = ref('')
|
||||||
const alertDialogMessage = ref('')
|
const alertDialogMessage = ref('')
|
||||||
|
const alertDialogRequirements = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
|
||||||
|
const alertDialogShowRequirements = ref(false)
|
||||||
|
|
||||||
// 资源类型配置(用于成本显示)
|
// 资源类型配置(用于成本显示)
|
||||||
const costResourceTypes = [
|
const costResourceTypes = [
|
||||||
@@ -186,8 +197,10 @@
|
|||||||
return t('researchView.research') // "研究"
|
return t('researchView.research') // "研究"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取前置条件列表文本
|
// 获取前置条件列表
|
||||||
const getRequirementsList = (techType: TechnologyType): string => {
|
const getRequirementsList = (
|
||||||
|
techType: TechnologyType
|
||||||
|
): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
|
||||||
const config = TECHNOLOGIES.value[techType]
|
const config = TECHNOLOGIES.value[techType]
|
||||||
const currentLevel = getTechLevel(techType)
|
const currentLevel = getTechLevel(techType)
|
||||||
const targetLevel = currentLevel + 1
|
const targetLevel = currentLevel + 1
|
||||||
@@ -195,28 +208,26 @@
|
|||||||
// 获取目标等级的所有前置条件(包括等级门槛)
|
// 获取目标等级的所有前置条件(包括等级门槛)
|
||||||
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
const requirements = publicLogic.getLevelRequirements(config, targetLevel)
|
||||||
|
|
||||||
if (!requirements || !planet.value) return ''
|
if (!requirements || !planet.value) return []
|
||||||
|
|
||||||
const lines: string[] = []
|
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
|
||||||
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
for (const [key, requiredLevel] of Object.entries(requirements)) {
|
||||||
// 检查是否为建筑类型
|
// 检查是否为建筑类型
|
||||||
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
if (Object.values(BuildingType).includes(key as BuildingType)) {
|
||||||
const bt = key as BuildingType
|
const bt = key as BuildingType
|
||||||
const currentLevel = planet.value.buildings[bt] || 0
|
const currentLevel = planet.value.buildings[bt] || 0
|
||||||
const name = BUILDINGS.value[bt]?.name || bt
|
const name = BUILDINGS.value[bt]?.name || bt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
// 检查是否为科技类型
|
// 检查是否为科技类型
|
||||||
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
|
||||||
const tt = key as TechnologyType
|
const tt = key as TechnologyType
|
||||||
const currentLevel = gameStore.player.technologies[tt] || 0
|
const currentLevel = gameStore.player.technologies[tt] || 0
|
||||||
const name = TECHNOLOGIES.value[tt]?.name || tt
|
const name = TECHNOLOGIES.value[tt]?.name || tt
|
||||||
const status = currentLevel >= requiredLevel ? '✓' : '✗'
|
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
|
||||||
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines.join('\n')
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// 研究科技
|
// 研究科技
|
||||||
@@ -224,7 +235,9 @@
|
|||||||
// 检查前置条件
|
// 检查前置条件
|
||||||
if (!checkUpgradeRequirements(techType)) {
|
if (!checkUpgradeRequirements(techType)) {
|
||||||
alertDialogTitle.value = t('common.requirementsNotMet')
|
alertDialogTitle.value = t('common.requirementsNotMet')
|
||||||
alertDialogMessage.value = getRequirementsList(techType)
|
alertDialogRequirements.value = getRequirementsList(techType)
|
||||||
|
alertDialogShowRequirements.value = true
|
||||||
|
alertDialogMessage.value = ''
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -233,6 +246,7 @@
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
alertDialogTitle.value = t('researchView.researchFailed')
|
alertDialogTitle.value = t('researchView.researchFailed')
|
||||||
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
alertDialogMessage.value = t('researchView.researchFailedMessage')
|
||||||
|
alertDialogShowRequirements.value = false
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +268,12 @@
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查队列中是否已存在该科技的研究任务
|
||||||
|
const existingQueueItem = player.value.researchQueue.find(item => item.type === 'technology' && item.itemType === techType)
|
||||||
|
if (existingQueueItem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 检查研究队列是否已满
|
// 检查研究队列是否已满
|
||||||
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
const maxQueue = publicLogic.getMaxResearchQueue(gameStore.player.technologies)
|
||||||
if (player.value.researchQueue.length >= maxQueue) {
|
if (player.value.researchQueue.length >= maxQueue) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||||
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
|
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
|
||||||
<Button @click="decreaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed <= 0.5">-</Button>
|
<Button @click="decreaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed <= 0.5">-</Button>
|
||||||
<span class="min-w-[60px] text-center font-medium">{{ gameStore.gameSpeed || 1 }}x</span>
|
<span class="min-w-[60px] text-center font-medium">{{ gameStore.gameSpeed }}x</span>
|
||||||
<Button @click="increaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed >= 10">+</Button>
|
<Button @click="increaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed >= 10">+</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button @click="resetSpeed" variant="ghost" size="sm">{{ t('settings.reset') }}</Button>
|
<Button @click="resetSpeed" variant="ghost" size="sm">{{ t('settings.reset') }}</Button>
|
||||||
@@ -89,6 +89,139 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- 通知设置 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{{ t('settings.notifications') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('settings.notificationsDesc') }}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- 浏览器通知 -->
|
||||||
|
<div class="flex flex-col gap-4 p-4 border rounded-lg">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.browserNotifications') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('settings.browserPermission') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch :checked="gameStore.notificationSettings?.browser" @update:checked="handleBrowserSwitch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面聚焦时不发送 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between pl-4 border-l-2"
|
||||||
|
:class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }"
|
||||||
|
>
|
||||||
|
<Label class="font-normal">{{ t('settings.suppressInFocus') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.suppressInFocus"
|
||||||
|
@update:checked="updateSuppressSetting"
|
||||||
|
:disabled="!gameStore.notificationSettings?.browser"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面内通知 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.inAppNotifications') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('settings.inAppNotificationsDesc') || t('settings.inAppNotifications') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch :checked="gameStore.notificationSettings?.inApp" @update:checked="(val: boolean) => updateInAppSetting(val)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 具体通知类型 -->
|
||||||
|
<div class="border rounded-lg overflow-hidden" :class="{ 'opacity-50 pointer-events-none': areMainSwitchesOff }">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-4 bg-muted/50 cursor-pointer select-none"
|
||||||
|
@click="!areMainSwitchesOff && (isTypesExpanded = !isTypesExpanded)"
|
||||||
|
>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.notificationTypes') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{
|
||||||
|
areMainSwitchesOff
|
||||||
|
? t('settings.notificationsDisabled')
|
||||||
|
: isTypesExpanded
|
||||||
|
? t('settings.collapseTypes')
|
||||||
|
: t('settings.expandTypes')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" class="h-8 w-8 p-0">
|
||||||
|
<component :is="isTypesExpanded ? ChevronUp : ChevronDown" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isTypesExpanded && !areMainSwitchesOff" class="p-4 space-y-4 border-t bg-card">
|
||||||
|
<!-- 建造完成 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class="font-normal cursor-pointer" @click="toggleType('construction')">{{ t('settings.constructionComplete') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.types.construction"
|
||||||
|
@update:checked="(val: boolean) => updateTypeSetting('construction', val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 研究完成 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class="font-normal cursor-pointer" @click="toggleType('research')">{{ t('settings.researchComplete') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.types.research"
|
||||||
|
@update:checked="(val: boolean) => updateTypeSetting('research', val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 帮助提示设置 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{{ t('hints.hintsEnabled') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('hints.hintsEnabledDesc') }}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- 启用/禁用提示 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('hints.hintsEnabled') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('hints.hintsEnabledDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch :checked="hintsEnabled" @update:checked="setHintsEnabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 重置提示 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('hints.resetHints') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('hints.resetHintsDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
<Button @click="handleResetHints" variant="outline">
|
||||||
|
<RotateCcw class="mr-2 h-4 w-4" />
|
||||||
|
{{ t('settings.reset') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 显示设置 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{{ t('settings.displaySettings') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('settings.displaySettingsDesc') }}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- 背景动画 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.backgroundAnimation') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('settings.backgroundAnimationDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch :checked="gameStore.player.backgroundEnabled ?? false" @update:checked="updateBackgroundSetting" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -119,6 +252,11 @@
|
|||||||
<div class="pt-2 border-t space-y-2">
|
<div class="pt-2 border-t space-y-2">
|
||||||
<h3 class="text-sm font-medium">{{ t('settings.community') }}</h3>
|
<h3 class="text-sm font-medium">{{ t('settings.community') }}</h3>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
<!-- Privacy Policy -->
|
||||||
|
<Button variant="outline" class="w-full justify-start" @click="openPrivacy">
|
||||||
|
<Shield class="mr-2 h-4 w-4" />
|
||||||
|
{{ t('settings.privacyPolicy') }}
|
||||||
|
</Button>
|
||||||
<!-- GitHub -->
|
<!-- GitHub -->
|
||||||
<Button variant="outline" class="w-full justify-start" @click="openGithub">
|
<Button variant="outline" class="w-full justify-start" @click="openGithub">
|
||||||
<ExternalLink class="mr-2 h-4 w-4" />
|
<ExternalLink class="mr-2 h-4 w-4" />
|
||||||
@@ -155,6 +293,9 @@
|
|||||||
|
|
||||||
<!-- 更新对话框 -->
|
<!-- 更新对话框 -->
|
||||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||||
|
|
||||||
|
<!-- 隐私协议弹窗 -->
|
||||||
|
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -164,6 +305,8 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -174,15 +317,31 @@
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw } from 'lucide-vue-next'
|
import {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
Trash2,
|
||||||
|
ExternalLink,
|
||||||
|
MessagesSquare,
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
RefreshCw,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
RotateCcw,
|
||||||
|
Shield
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../package.json'
|
||||||
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
|
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
|
||||||
import type { VersionInfo } from '@/utils/versionCheck'
|
import type { VersionInfo } from '@/utils/versionCheck'
|
||||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||||
|
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||||
|
import { useHints } from '@/composables/useHints'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
const fileInputRef = ref<HTMLInputElement>()
|
const fileInputRef = ref<HTMLInputElement>()
|
||||||
@@ -195,6 +354,70 @@
|
|||||||
const confirmMessage = ref('')
|
const confirmMessage = ref('')
|
||||||
let confirmCallback: (() => void) | null = null
|
let confirmCallback: (() => void) | null = null
|
||||||
|
|
||||||
|
const isTypesExpanded = ref(false)
|
||||||
|
|
||||||
|
// 确保通知设置存在
|
||||||
|
if (!gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings = {
|
||||||
|
browser: false,
|
||||||
|
inApp: true,
|
||||||
|
suppressInFocus: false,
|
||||||
|
types: { construction: true, research: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const areMainSwitchesOff = computed(() => {
|
||||||
|
const s = gameStore.notificationSettings
|
||||||
|
return !s?.browser && !s?.inApp
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当主开关关闭时自动折叠
|
||||||
|
// watch(areMainSwitchesOff, (val) => {
|
||||||
|
// if (val) isTypesExpanded.value = false
|
||||||
|
// })
|
||||||
|
|
||||||
|
const updateInAppSetting = (val: boolean) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.inApp = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSuppressSetting = (val: boolean) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.suppressInFocus = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTypeSetting = (key: 'construction' | 'research', val: boolean) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.types[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleType = (key: 'construction' | 'research') => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
const current = gameStore.notificationSettings.types[key]
|
||||||
|
gameStore.notificationSettings.types[key] = !current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBrowserSwitch = async (checked: boolean) => {
|
||||||
|
if (!gameStore.notificationSettings) return
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
const granted = await gameStore.requestBrowserPermission()
|
||||||
|
if (granted) {
|
||||||
|
gameStore.notificationSettings.browser = true
|
||||||
|
toast.success(t('settings.permissionGranted'))
|
||||||
|
} else {
|
||||||
|
gameStore.notificationSettings.browser = false
|
||||||
|
toast.error(t('settings.permissionDenied'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gameStore.notificationSettings.browser = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算是否可以检查版本(主动检测:5分钟内不能重复检查)
|
// 计算是否可以检查版本(主动检测:5分钟内不能重复检查)
|
||||||
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
|
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
|
||||||
|
|
||||||
@@ -240,6 +463,14 @@
|
|||||||
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
|
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 隐私协议弹窗状态
|
||||||
|
const showPrivacyDialog = ref(false)
|
||||||
|
|
||||||
|
// 打开隐私协议弹窗
|
||||||
|
const openPrivacy = () => {
|
||||||
|
showPrivacyDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 手动检查版本
|
// 手动检查版本
|
||||||
const showUpdateDialog = ref(false)
|
const showUpdateDialog = ref(false)
|
||||||
const updateInfo = ref<VersionInfo | null>(null)
|
const updateInfo = ref<VersionInfo | null>(null)
|
||||||
@@ -369,6 +600,7 @@
|
|||||||
|
|
||||||
// 清除数据
|
// 清除数据
|
||||||
const handleClearData = () => {
|
const handleClearData = () => {
|
||||||
|
gameStore.isPaused = true
|
||||||
confirmTitle.value = t('settings.clearConfirmTitle')
|
confirmTitle.value = t('settings.clearConfirmTitle')
|
||||||
confirmMessage.value = t('settings.clearConfirmMessage')
|
confirmMessage.value = t('settings.clearConfirmMessage')
|
||||||
showConfirmDialog.value = true
|
showConfirmDialog.value = true
|
||||||
@@ -438,4 +670,15 @@
|
|||||||
fileInputRef.value.value = ''
|
fileInputRef.value.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置提示
|
||||||
|
const handleResetHints = () => {
|
||||||
|
resetHints()
|
||||||
|
toast.success(t('hints.resetHints'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新背景设置
|
||||||
|
const updateBackgroundSetting = (val: boolean) => {
|
||||||
|
gameStore.player.backgroundEnabled = val
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div
|
<div
|
||||||
class="h-full transition-all duration-300"
|
class="h-full transition-all duration-300"
|
||||||
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
:class="fleetStorageUsage > maxFleetStorage ? 'bg-destructive' : 'bg-primary'"
|
||||||
:style="{ width: `${Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100)}%` }"
|
:style="{ width: `${maxFleetStorage > 0 ? Math.min((fleetStorageUsage / maxFleetStorage) * 100, 100) : 0}%` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,13 +216,13 @@
|
|||||||
[ShipType.Deathstar]: 0
|
[ShipType.Deathstar]: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildShip = (shipType: ShipType, quantity: number): boolean => {
|
const buildShip = (shipType: ShipType, quantity: number): { success: boolean; reason?: string } => {
|
||||||
if (!gameStore.currentPlanet) return 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 false
|
if (!validation.valid) return { success: false, reason: validation.reason }
|
||||||
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 true
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建造舰船
|
// 建造舰船
|
||||||
@@ -235,10 +235,10 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = buildShip(shipType, quantity)
|
const result = buildShip(shipType, quantity)
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
alertDialogTitle.value = t('shipyardView.buildFailed')
|
alertDialogTitle.value = t('shipyardView.buildFailed')
|
||||||
alertDialogMessage.value = t('shipyardView.buildFailedMessage')
|
alertDialogMessage.value = result.reason ? t(result.reason) : t('shipyardView.buildFailedMessage')
|
||||||
alertDialogOpen.value = true
|
alertDialogOpen.value = true
|
||||||
} else {
|
} else {
|
||||||
quantities.value[shipType] = 0
|
quantities.value[shipType] = 0
|
||||||
@@ -260,6 +260,11 @@
|
|||||||
darkMatter: config.cost.darkMatter * quantity
|
darkMatter: config.cost.darkMatter * quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查舰队仓储空间是否足够
|
||||||
|
if (!fleetStorageLogic.hasEnoughFleetStorage(planet.value, shipType, quantity, gameStore.player.technologies)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
publicLogic.checkRequirements(planet.value, gameStore.player.technologies, config.requirements) &&
|
||||||
planet.value.resources.metal >= totalCost.metal &&
|
planet.value.resources.metal >= totalCost.metal &&
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user