Compare commits
7 Commits
cb412544e9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b851656de | |||
| ccfd6a5e75 | |||
| 27e1c8d440 | |||
| 5d4371ba1f | |||
| cef974d94f | |||
| 16223c89a5 | |||
| 021f6a54f5 |
@@ -12,7 +12,11 @@
|
|||||||
"Bash(git config:*)",
|
"Bash(git config:*)",
|
||||||
"Bash(git remote add:*)",
|
"Bash(git remote add:*)",
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Read(//d/uniapp/app_test/wwa3/**)",
|
||||||
|
"Bash(npx eslint:*)",
|
||||||
|
"mcp__playwright__browser_close"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,4 +243,185 @@ page {
|
|||||||
background-color: $bg-tertiary;
|
background-color: $bg-tertiary;
|
||||||
border-radius: 4rpx;
|
border-radius: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 过渡动画 ==================== */
|
||||||
|
|
||||||
|
/* 淡入淡出 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑动进入 - 从右侧 */
|
||||||
|
.slide-right-enter-active,
|
||||||
|
.slide-right-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑动进入 - 从左侧 */
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-left-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-leave-to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滑动进入 - 从下方 */
|
||||||
|
.slide-up-enter-active,
|
||||||
|
.slide-up-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-enter-from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩放动画 */
|
||||||
|
.scale-enter-active,
|
||||||
|
.scale-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-enter-from,
|
||||||
|
.scale-leave-to {
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹跳动画 */
|
||||||
|
.bounce-enter-active {
|
||||||
|
animation: bounce-in 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce-leave-active {
|
||||||
|
animation: bounce-in 0.3s ease reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-in {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 闪烁动画 */
|
||||||
|
@keyframes flash {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
animation: flash 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 脉冲动画 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 摇晃动画 */
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-10rpx); }
|
||||||
|
75% { transform: translateX(10rpx); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转加载 */
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打字机效果 */
|
||||||
|
@keyframes typewriter {
|
||||||
|
from { width: 0; }
|
||||||
|
to { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 工具类 ==================== */
|
||||||
|
|
||||||
|
/* 过渡效果 */
|
||||||
|
.transition-all {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-fast {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-slow {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.hover-scale {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scale:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-bright {
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-bright:active {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a text-based idle adventure game (文字冒险游戏) built with uni-app, Vue 3, and Pinia. The game features skill-driven progression, idle mechanics, combat, crafting, and a dark post-apocalyptic theme.
|
||||||
|
|
||||||
|
**Tech Stack:**
|
||||||
|
- Framework: uni-app (Vue 3 with Composition API)
|
||||||
|
- State Management: Pinia
|
||||||
|
- Testing: Jest + Vitest + Playwright
|
||||||
|
- Style: SCSS with global variables
|
||||||
|
|
||||||
|
## Build and Run Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- Use HBuilderX IDE to run the project (uni-app standard)
|
||||||
|
- Run to different platforms: mp-weixin (WeChat Mini Program), h5, app
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
npm test # Run Jest tests in watch mode
|
||||||
|
npm run test:watch # Run tests in watch mode
|
||||||
|
npm run test:coverage # Generate coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
├── components/
|
||||||
|
│ ├── common/ # Reusable UI components (ProgressBar, StatItem, etc.)
|
||||||
|
│ ├── layout/ # Layout components (TabBar, Drawer)
|
||||||
|
│ ├── panels/ # Main game panels (StatusPanel, MapPanel, LogPanel)
|
||||||
|
│ └── drawers/ # Slide-out drawers (Inventory, Event, Shop, Crafting)
|
||||||
|
├── config/ # Game configuration (skills, items, enemies, locations, etc.)
|
||||||
|
├── store/ # Pinia stores (player.js, game.js)
|
||||||
|
├── utils/ # Game systems (combat, skill, item, task, event, etc.)
|
||||||
|
├── pages/ # Uni-app pages
|
||||||
|
└── tests/ # Unit tests with fixtures and helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Systems (utils/)
|
||||||
|
- **skillSystem.js**: Skill unlocking, XP, leveling, milestone rewards, parent skills
|
||||||
|
- **combatSystem.js**: AP/EP hit calculation, three-layer defense (evade/shield/armor), damage
|
||||||
|
- **itemSystem.js**: Item usage, equipment, quality calculation, inventory management
|
||||||
|
- **taskSystem.js**: Idle task management, mutual exclusion checks
|
||||||
|
- **eventSystem.js**: Event triggering, NPC dialogue handling
|
||||||
|
- **environmentSystem.js**: Environment penalties, passive skill XP
|
||||||
|
- **gameLoop.js**: Main game tick (1-second interval), time progression, auto-save
|
||||||
|
- **storage.js**: Save/load using uni.setStorageSync, offline earnings
|
||||||
|
- **craftingSystem.js**: Recipe-based crafting with quality calculation
|
||||||
|
- **levelingSystem.js**: Player level progression and stat scaling
|
||||||
|
- **mapLayout.js**: Visual map representation
|
||||||
|
|
||||||
|
### State Management (Pinia Stores)
|
||||||
|
- **player.js**: Base stats, current stats (HP/stamina/sanity), level, skills, equipment, inventory, currency, location, flags
|
||||||
|
- **game.js**: Current tab, drawer states, logs, game time, combat state, active tasks, market prices, current event
|
||||||
|
|
||||||
|
### Configuration (config/)
|
||||||
|
- **constants.js**: Game constants (quality levels, stat scaling, time scale, etc.)
|
||||||
|
- **skills.js**: All skill definitions with milestones and unlock conditions
|
||||||
|
- **items.js**: All item definitions (weapons, consumables, materials, books)
|
||||||
|
- **enemies.js**: Enemy stats, drops, spawn conditions
|
||||||
|
- **locations.js**: Area definitions, connections, activities, unlock conditions
|
||||||
|
- **npcs.js**: NPC dialogue trees and interactions
|
||||||
|
- **events.js**: Event definitions and triggers
|
||||||
|
- **recipes.js**: Crafting recipes
|
||||||
|
- **shop.js**: Shop inventory and pricing
|
||||||
|
- **formulas.js**: Mathematical formulas for stat calculations
|
||||||
|
|
||||||
|
## Key Game Concepts
|
||||||
|
|
||||||
|
### Quality System (品质系统)
|
||||||
|
Items have quality (0-250) that affects their power:
|
||||||
|
- 0-49: 垃圾 (Trash)
|
||||||
|
- 50-99: 普通 (Common)
|
||||||
|
- 100-139: 优秀 (Good)
|
||||||
|
- 140-169: 稀有 (Rare)
|
||||||
|
- 170-199: 史诗 (Epic)
|
||||||
|
- 200-250: 传说 (Legendary)
|
||||||
|
|
||||||
|
Each tier has a multiplier that increases item stats.
|
||||||
|
|
||||||
|
### AP/EP Combat System
|
||||||
|
- AP (Attack Points) = 灵巧 + 智力×0.2 + skills + equipment + stance - enemyCountPenalty
|
||||||
|
- EP (Evasion Points) = 敏捷 + 智力×0.2 + skills + equipment + stance - environmentPenalty
|
||||||
|
- Hit rate is non-linear: AP=5×EP → ~98%, AP=EP → ~50%, AP=0.5×EP → ~24%
|
||||||
|
|
||||||
|
### Three-Layer Defense
|
||||||
|
1. Evasion (complete dodge)
|
||||||
|
2. Shield absorption
|
||||||
|
3. Defense reduction (min 10% damage)
|
||||||
|
|
||||||
|
### Skill Milestones
|
||||||
|
Skills provide permanent global bonuses at specific levels (e.g., all weapons +crit rate at level 5). Parent skills auto-level to match highest child skill level.
|
||||||
|
|
||||||
|
### Time System
|
||||||
|
- 1 real second = 5 game minutes
|
||||||
|
- Day/night cycle affects gameplay
|
||||||
|
- Markets refresh daily
|
||||||
|
- Offline earnings available (0.5h base, 2.5h with ad)
|
||||||
|
|
||||||
|
### Idle Tasks
|
||||||
|
Active tasks (reading, training, working) and passive tasks (environment adaptation). Combat is mutually exclusive with most tasks.
|
||||||
|
|
||||||
|
## Important Patterns
|
||||||
|
|
||||||
|
### Adding New Skills
|
||||||
|
1. Add config to `config/skills.js`
|
||||||
|
2. Set unlockCondition (item, location, or skill level)
|
||||||
|
3. Add milestones with permanent bonuses
|
||||||
|
4. Link to parentSkill if applicable
|
||||||
|
|
||||||
|
### Adding New Items
|
||||||
|
1. Add config to `config/items.js`
|
||||||
|
2. Set baseValue, baseDamage/defense, type
|
||||||
|
3. Add to shop or enemy drop tables
|
||||||
|
4. For equipment, quality affects final stats
|
||||||
|
|
||||||
|
### Adding New Locations
|
||||||
|
1. Add to `config/locations.js`
|
||||||
|
2. Set connections, enemies, activities
|
||||||
|
3. Add unlockCondition if needed
|
||||||
|
4. Update `utils/mapLayout.js` for visual representation
|
||||||
|
|
||||||
|
### Store Usage
|
||||||
|
```javascript
|
||||||
|
import { usePlayerStore } from '@/store/player.js'
|
||||||
|
import { useGameStore } from '@/store/game.js'
|
||||||
|
|
||||||
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
|
|
||||||
|
// Access state
|
||||||
|
player.baseStats.strength
|
||||||
|
game.logs
|
||||||
|
|
||||||
|
// Call actions
|
||||||
|
game.addLog('message', 'info')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Variables (uni.scss)
|
||||||
|
- $bg-primary: #0a0a0f (main background)
|
||||||
|
- $accent: #4ecdc4 (primary accent)
|
||||||
|
- $danger: #ff6b6b (combat/HP low)
|
||||||
|
- Quality colors: $quality-trash through $quality-legend
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Game Loop Timing
|
||||||
|
- Main loop runs every 1 second via `gameLoop.js`
|
||||||
|
- Time advances by TIME_SCALE (5) minutes per tick
|
||||||
|
- Auto-save triggers every 30 game minutes
|
||||||
|
|
||||||
|
### Saving
|
||||||
|
- `App.vue` handles onLaunch/onShow/onHide
|
||||||
|
- Auto-saves on app hide
|
||||||
|
- Manual save via `window.manualSave()`
|
||||||
|
- Logs are NOT persisted (cleared on load)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Tests in `tests/unit/` use Vitest
|
||||||
|
- Fixtures in `tests/fixtures/` provide mock data
|
||||||
|
- Helpers in `tests/helpers/` mock timers, stores, random
|
||||||
|
- See `tests/README.md` for testing guidelines
|
||||||
|
|
||||||
|
## Known Gotchas
|
||||||
|
|
||||||
|
1. **uni API availability**: Some uni APIs only work in specific platforms (H5 vs mini-program)
|
||||||
|
2. **Store timing**: Always use `setActivePinia(createPinia())` in tests
|
||||||
|
3. **Logs array**: Limited to 200 entries, shifts old entries
|
||||||
|
4. **Skill level cap**: Cannot exceed player level × 2
|
||||||
|
5. **Equipment quality**: Calculated as base × quality% × tierMultiplier
|
||||||
|
6. **Combat stance**: Switching stance resets attack progress
|
||||||
|
7. **Market saturation**: Selling same item reduces price (encourages exploration)
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
# 文字冒险游戏 - 内容开发指南
|
||||||
|
|
||||||
|
本项目是一个基于 UniApp + Vue 3 的文字冒险游戏,采用配置驱动的架构。所有游戏内容(物品、NPC、技能、配方、事件等)都通过配置文件定义,无需修改核心代码即可扩展游戏内容。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── constants.js # 游戏常量(品质等级、时间等)
|
||||||
|
├── items.js # 物品配置
|
||||||
|
├── skills.js # 技能配置
|
||||||
|
├── npcs.js # NPC配置
|
||||||
|
├── recipes.js # 制造配方配置
|
||||||
|
├── enemies.js # 敌人配置
|
||||||
|
├── events.js # 事件/剧情配置
|
||||||
|
├── locations.js # 地点配置
|
||||||
|
└── shop.js # 商店配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 创建物品
|
||||||
|
|
||||||
|
物品定义在 `config/items.js` 中的 `ITEM_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_item_id: {
|
||||||
|
id: 'your_item_id', // 唯一ID(建议使用下划线命名)
|
||||||
|
name: '物品名称', // 显示名称
|
||||||
|
type: 'weapon', // 类型:weapon/armor/shield/accessory/consumable/book/material
|
||||||
|
subtype: 'one_handed', // 子类型(可选)
|
||||||
|
icon: '🗡️', // 图标(emoji)
|
||||||
|
baseValue: 100, // 基础价值(铜币)
|
||||||
|
description: '物品描述', // 描述文本
|
||||||
|
stackable: true, // 是否可堆叠(消耗品/素材)
|
||||||
|
maxStack: 99 // 最大堆叠数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 武器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sword: {
|
||||||
|
id: 'sword',
|
||||||
|
name: '铁剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword', // one_handed/two_handed/sword/axe/blunt/ranged
|
||||||
|
icon: '⚔️',
|
||||||
|
baseValue: 500,
|
||||||
|
baseDamage: 25, // 基础攻击力
|
||||||
|
attackSpeed: 1.2, // 攻击速度倍率
|
||||||
|
quality: 100, // 基础品质(影响属性计算)
|
||||||
|
unlockSkill: 'sword_mastery', // 装备解锁的技能
|
||||||
|
stats: { // 额外属性加成
|
||||||
|
critRate: 5 // 暴击率+5%
|
||||||
|
},
|
||||||
|
description: '一把精工打造的铁剑。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 防具
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
armor: {
|
||||||
|
id: 'leather_armor',
|
||||||
|
name: '皮甲',
|
||||||
|
type: 'armor',
|
||||||
|
subtype: 'light', // light/heavy
|
||||||
|
icon: '🦺',
|
||||||
|
baseValue: 150,
|
||||||
|
baseDefense: 8, // 基础防御力
|
||||||
|
quality: 100,
|
||||||
|
stats: {
|
||||||
|
evasion: 5 // 闪避+5%
|
||||||
|
},
|
||||||
|
description: '用兽皮制成的轻甲。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消耗品
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
potion: {
|
||||||
|
id: 'health_potion',
|
||||||
|
name: '治疗药水',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'medicine', // food/drink/medicine
|
||||||
|
icon: '🧪',
|
||||||
|
baseValue: 150,
|
||||||
|
effect: { // 使用效果
|
||||||
|
health: 100, // 恢复生命值
|
||||||
|
stamina: 20, // 恢复耐力
|
||||||
|
sanity: 10 // 恢复精神
|
||||||
|
},
|
||||||
|
description: '快速恢复生命值。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 书籍
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
book: {
|
||||||
|
id: 'skill_book',
|
||||||
|
name: '战斗指南',
|
||||||
|
type: 'book',
|
||||||
|
icon: '📖',
|
||||||
|
baseValue: 200,
|
||||||
|
readingTime: 60, // 阅读时间(秒)
|
||||||
|
expReward: { // 阅读获得的技能经验
|
||||||
|
combat: 100, // 主经验
|
||||||
|
sword_mastery: 50 // 特定技能经验
|
||||||
|
},
|
||||||
|
completionBonus: { // 完成奖励(可选)
|
||||||
|
exp: 200 // 额外主经验
|
||||||
|
},
|
||||||
|
description: '记载着战斗技巧的书籍。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 创建技能
|
||||||
|
|
||||||
|
技能定义在 `config/skills.js` 中的 `SKILL_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_skill: {
|
||||||
|
id: 'your_skill',
|
||||||
|
name: '技能名称',
|
||||||
|
type: 'combat', // 类型:combat/life/passive
|
||||||
|
category: 'weapon', // 分类:weapon/crafting/reading等
|
||||||
|
icon: '⚔️',
|
||||||
|
maxLevel: 20, // 最大等级
|
||||||
|
expPerLevel: (level) => level * 100, // 每级所需经验公式
|
||||||
|
milestones: { // 里程碑(等级解锁效果)
|
||||||
|
5: {
|
||||||
|
desc: '攻击力+10%',
|
||||||
|
effect: { attackBonus: 10 }
|
||||||
|
},
|
||||||
|
10: {
|
||||||
|
desc: '解锁新技能',
|
||||||
|
effect: { unlockSkill: 'advanced_skill' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unlockCondition: { // 解锁条件(可选)
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'starter_sword'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 战斗技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sword_mastery: {
|
||||||
|
id: 'sword_mastery',
|
||||||
|
name: '剑术精通',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'weapon',
|
||||||
|
icon: '⚔️',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 100,
|
||||||
|
milestones: {
|
||||||
|
5: { desc: '暴击率+5%', effect: { critRate: 5 } },
|
||||||
|
10: { desc: '攻击力+15%', effect: { attackBonus: 15 } },
|
||||||
|
15: { desc: '暴击伤害+0.5', effect: { critMult: 0.5 } },
|
||||||
|
20: { desc: '剑术大师', effect: { mastery: true } }
|
||||||
|
},
|
||||||
|
unlockCondition: null // 初始解锁/通过装备武器解锁
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生活技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
crafting: {
|
||||||
|
id: 'crafting',
|
||||||
|
name: '制造',
|
||||||
|
type: 'life',
|
||||||
|
category: 'crafting',
|
||||||
|
icon: '🔨',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 80,
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁基础制造', effect: {} },
|
||||||
|
5: { desc: '制造成功率+5%', effect: { craftingSuccessRate: 5 } },
|
||||||
|
10: { desc: '制造时间-25%', effect: { craftingSpeed: 0.25 } }
|
||||||
|
},
|
||||||
|
unlockCondition: null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 被动技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
night_vision: {
|
||||||
|
id: 'night_vision',
|
||||||
|
name: '夜视',
|
||||||
|
type: 'passive',
|
||||||
|
category: 'environment',
|
||||||
|
icon: '👁️',
|
||||||
|
maxLevel: 10,
|
||||||
|
expPerLevel: (level) => level * 30,
|
||||||
|
milestones: {
|
||||||
|
5: { desc: '黑暗惩罚-10%', effect: { darkPenaltyReduce: 10 } },
|
||||||
|
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
location: 'basement' // 进入该地点自动解锁
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 创建NPC
|
||||||
|
|
||||||
|
NPC定义在 `config/npcs.js` 中的 `NPC_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_npc: {
|
||||||
|
id: 'your_npc',
|
||||||
|
name: 'NPC名称',
|
||||||
|
location: 'camp', // 所在位置(location ID)
|
||||||
|
dialogue: {
|
||||||
|
first: { // 首次对话
|
||||||
|
text: '对话文本...',
|
||||||
|
choices: [
|
||||||
|
{ text: '选项1', next: 'node1' },
|
||||||
|
{ text: '选项2', next: null, action: 'action_name' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
node1: { // 后续对话节点
|
||||||
|
text: '更多对话...',
|
||||||
|
choices: [
|
||||||
|
{ text: '再见', next: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 带动作的NPC
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
merchant: {
|
||||||
|
id: 'merchant',
|
||||||
|
name: '商人',
|
||||||
|
location: 'market',
|
||||||
|
dialogue: {
|
||||||
|
first: {
|
||||||
|
text: '欢迎光临!',
|
||||||
|
choices: [
|
||||||
|
{ text: '查看商品', next: null, action: 'open_shop' },
|
||||||
|
{ text: '这是哪里?', next: 'explain' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explain: {
|
||||||
|
text: '这里是市场...',
|
||||||
|
choices: [
|
||||||
|
{ text: '查看商品', next: null, action: 'open_shop' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可用动作
|
||||||
|
|
||||||
|
| 动作 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `open_shop` | 打开商店 |
|
||||||
|
| `give_item` | 给予物品(需指定 actionData) |
|
||||||
|
| `heal` | 恢复生命值 |
|
||||||
|
| `unlock_skill` | 解锁技能 |
|
||||||
|
| `teleport` | 传送到指定位置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 创建配方
|
||||||
|
|
||||||
|
配方定义在 `config/recipes.js` 中的 `RECIPE_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_recipe: {
|
||||||
|
id: 'your_recipe',
|
||||||
|
type: RECIPE_TYPES.WEAPON, // 类型:WEAPON/ARMOR/SHIELD/CONSUMABLE/SPECIAL
|
||||||
|
resultItem: 'result_item_id', // 产物物品ID
|
||||||
|
resultCount: 1, // 产物数量
|
||||||
|
materials: [ // 材料需求
|
||||||
|
{ itemId: 'material_1', count: 5 },
|
||||||
|
{ itemId: 'material_2', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting', // 所需技能
|
||||||
|
requiredSkillLevel: 3, // 所需技能等级
|
||||||
|
baseTime: 60, // 基础制造时间(秒)
|
||||||
|
baseSuccessRate: 0.85, // 基础成功率(0-1)
|
||||||
|
unlockCondition: { // 解锁条件(可选)
|
||||||
|
type: 'skill', // skill/quest/item
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配方示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
iron_sword: {
|
||||||
|
id: 'iron_sword',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'iron_sword',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 5 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'sword_mastery',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 180,
|
||||||
|
baseSuccessRate: 0.75,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 创建敌人
|
||||||
|
|
||||||
|
敌人定义在 `config/enemies.js` 中的 `ENEMY_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_enemy: {
|
||||||
|
id: 'your_enemy',
|
||||||
|
name: '敌人名称',
|
||||||
|
icon: '👾',
|
||||||
|
level: 5, // 等级
|
||||||
|
hp: 100, // 生命值
|
||||||
|
attack: 15, // 攻击力
|
||||||
|
defense: 5, // 防御力
|
||||||
|
speed: 10, // 速度(影响行动顺序)
|
||||||
|
expReward: 50, // 经验奖励
|
||||||
|
// 可选属性
|
||||||
|
critRate: 5, // 暴击率
|
||||||
|
fleeRate: 20, // 逃跑成功率
|
||||||
|
skills: ['skill_id'], // 拥有的技能
|
||||||
|
drops: [ // 掉落列表
|
||||||
|
{ itemId: 'drop_item', chance: 0.3, count: { min: 1, max: 2 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 创建事件/剧情
|
||||||
|
|
||||||
|
事件定义在 `config/events.js` 中的 `EVENT_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_event: {
|
||||||
|
id: 'your_event',
|
||||||
|
type: 'story', // 类型:story/tips/unlock/dialogue/reward/combat
|
||||||
|
title: '事件标题',
|
||||||
|
text: '事件内容...',
|
||||||
|
textTemplate: '支持{variable}变量替换', // 或使用模板
|
||||||
|
choices: [
|
||||||
|
{ text: '选项', next: 'next_event_id' },
|
||||||
|
{ text: '带动作的选项', next: null, action: 'action_name', actionData: {} }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件类型
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `story` | 剧情事件 |
|
||||||
|
| `tips` | 提示信息 |
|
||||||
|
| `unlock` | 解锁内容提示 |
|
||||||
|
| `dialogue` | NPC对话 |
|
||||||
|
| `reward` | 奖励提示 |
|
||||||
|
| `combat` | 战斗事件 |
|
||||||
|
|
||||||
|
### 触发条件
|
||||||
|
|
||||||
|
在 `EVENT_TRIGGERS` 中配置触发条件:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const EVENT_TRIGGERS = {
|
||||||
|
once: {
|
||||||
|
your_trigger: {
|
||||||
|
condition: (playerStore) => {
|
||||||
|
// 返回 true 时触发事件
|
||||||
|
return playerStore.level >= 5 && !playerStore.flags.yourEventSeen
|
||||||
|
},
|
||||||
|
eventId: 'your_event',
|
||||||
|
setFlag: 'yourEventSeen' // 触发后设置的标记
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
your_env_trigger: {
|
||||||
|
condition: (playerStore, locationId) => {
|
||||||
|
return locationId === 'dark_cave' && !playerStore.flags.darkCaveFirstEnter
|
||||||
|
},
|
||||||
|
eventId: 'dark_warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 解锁条件
|
||||||
|
|
||||||
|
解锁条件可用于技能、配方、地点等。
|
||||||
|
|
||||||
|
### 类型
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基于等级
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'level',
|
||||||
|
level: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于技能
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于物品
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'key_item'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于位置
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'location',
|
||||||
|
locationId: 'secret_area'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于任务标记
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'quest',
|
||||||
|
flag: 'completed_quest_x'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 品质系统
|
||||||
|
|
||||||
|
物品品质定义在 `config/constants.js` 中的 `QUALITY_LEVELS`。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { level: 1, name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { level: 2, name: '普通', color: '#ffffff', range: [50, 89], multiplier: 1.0 },
|
||||||
|
3: { level: 3, name: '优秀', color: '#22c55e', range: [90, 129], multiplier: 1.2 },
|
||||||
|
4: { level: 4, name: '稀有', color: '#3b82f6', range: [130, 159], multiplier: 1.5 },
|
||||||
|
5: { level: 5, name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 2.0 },
|
||||||
|
6: { level: 6, name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 快速开始示例
|
||||||
|
|
||||||
|
### 添加新武器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 config/items.js 中添加
|
||||||
|
flame_sword: {
|
||||||
|
id: 'flame_sword',
|
||||||
|
name: '烈焰之剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword',
|
||||||
|
icon: '🔥⚔️',
|
||||||
|
baseValue: 2000,
|
||||||
|
baseDamage: 50,
|
||||||
|
attackSpeed: 1.3,
|
||||||
|
quality: 150,
|
||||||
|
unlockSkill: 'sword_mastery',
|
||||||
|
stats: {
|
||||||
|
critRate: 10,
|
||||||
|
fireDamage: 15
|
||||||
|
},
|
||||||
|
description: '燃烧着永恒烈焰的魔剑。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 config/skills.js 中添加
|
||||||
|
fire_mastery: {
|
||||||
|
id: 'fire_mastery',
|
||||||
|
name: '火焰精通',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'element',
|
||||||
|
icon: '🔥',
|
||||||
|
maxLevel: 15,
|
||||||
|
expPerLevel: (level) => level * 150,
|
||||||
|
milestones: {
|
||||||
|
3: { desc: '火焰伤害+20%', effect: { fireDamageBonus: 20 } },
|
||||||
|
7: { desc: '攻击附带燃烧', effect: { burnEffect: true } },
|
||||||
|
15: { desc: '火焰免疫', effect: { fireImmunity: true } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'flame_essence'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 注意事项
|
||||||
|
|
||||||
|
1. **ID命名**:统一使用下划线命名(snake_case),保持唯一性
|
||||||
|
2. **引用**:添加新物品后,记得在配方、商店、敌人掉落等处引用
|
||||||
|
3. **平衡性**:注意数值平衡,参考现有物品的属性比例
|
||||||
|
4. **测试**:添加内容后建议在游戏中测试效果
|
||||||
|
5. **保存**:游戏进度保存在 localStorage,修改配置后可能需要清除缓存测试
|
||||||
@@ -0,0 +1,597 @@
|
|||||||
|
<template>
|
||||||
|
<view class="mini-map">
|
||||||
|
<!-- 地图容器 -->
|
||||||
|
<view class="mini-map__container">
|
||||||
|
<scroll-view
|
||||||
|
class="mini-map__scroll"
|
||||||
|
scroll-x
|
||||||
|
scroll-y
|
||||||
|
:scroll-left="scrollLeft"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="mini-map__content"
|
||||||
|
:style="contentStyle"
|
||||||
|
>
|
||||||
|
<!-- 连接线层 -->
|
||||||
|
<view class="mini-map__connections">
|
||||||
|
<view
|
||||||
|
v-for="edge in mapData.edges"
|
||||||
|
:key="`${edge.from}-${edge.to}`"
|
||||||
|
class="connection-line"
|
||||||
|
:class="{ 'connection-line--path': isInPath(edge) }"
|
||||||
|
:style="getLineStyle(edge)"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 节点层 -->
|
||||||
|
<view
|
||||||
|
v-for="node in mapData.nodes"
|
||||||
|
:key="node.id"
|
||||||
|
class="map-node"
|
||||||
|
:class="getNodeClass(node)"
|
||||||
|
:style="getNodeStyle(node)"
|
||||||
|
@click="handleNodeClick(node)"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
|
<!-- 节点圆点 -->
|
||||||
|
<view class="node__dot"></view>
|
||||||
|
|
||||||
|
<!-- 当前位置脉冲效果 -->
|
||||||
|
<view v-if="node.id === currentLocation" class="node__pulse"></view>
|
||||||
|
|
||||||
|
<!-- 节点名称 -->
|
||||||
|
<text class="node__name">{{ node.name }}</text>
|
||||||
|
|
||||||
|
<!-- 锁定图标 -->
|
||||||
|
<text v-if="isLocked(node)" class="node__lock">🔒</text>
|
||||||
|
|
||||||
|
<!-- 距离标记 -->
|
||||||
|
<text v-else-if="getDistance(node) > 0" class="node__distance">{{ getDistance(node) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 缩放控制 -->
|
||||||
|
<view class="mini-map__zoom-controls">
|
||||||
|
<view class="zoom-btn" @click="zoomIn">
|
||||||
|
<text class="zoom-icon">+</text>
|
||||||
|
</view>
|
||||||
|
<view class="zoom-btn" @click="resetZoom">
|
||||||
|
<text class="zoom-icon">⟲</text>
|
||||||
|
</view>
|
||||||
|
<view class="zoom-btn" @click="zoomOut">
|
||||||
|
<text class="zoom-icon">−</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图例 -->
|
||||||
|
<view class="mini-map__legend">
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot--current"></view>
|
||||||
|
<text class="legend-text">当前位置</text>
|
||||||
|
</view>
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot--safe"></view>
|
||||||
|
<text class="legend-text">安全</text>
|
||||||
|
</view>
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot--danger"></view>
|
||||||
|
<text class="legend-text">危险</text>
|
||||||
|
</view>
|
||||||
|
<view class="legend-item">
|
||||||
|
<view class="legend-dot legend-dot--dungeon"></view>
|
||||||
|
<text class="legend-text">副本</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 路径提示 -->
|
||||||
|
<view v-if="selectedPath.length > 1" class="mini-map__path">
|
||||||
|
<text class="path-text">前往路径: {{ selectedPath.map(id => getLocationName(id)).join(' → ') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useGameStore } from '@/store/game'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { LOCATION_CONFIG } from '@/config/locations'
|
||||||
|
import { getMapData, getPath } from '@/utils/mapLayout.js'
|
||||||
|
|
||||||
|
const game = useGameStore()
|
||||||
|
const player = usePlayerStore()
|
||||||
|
|
||||||
|
const mapData = ref({ nodes: [], edges: [], width: 300, height: 400 })
|
||||||
|
const selectedPath = ref([])
|
||||||
|
|
||||||
|
// 缩放和平移状态
|
||||||
|
const zoom = ref(1)
|
||||||
|
const scrollLeft = ref(0)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
const currentLocationNode = ref(null)
|
||||||
|
|
||||||
|
// 容器尺寸
|
||||||
|
const containerWidth = 280
|
||||||
|
const containerHeight = 300
|
||||||
|
|
||||||
|
// 触摸拖动状态
|
||||||
|
const touchStartX = ref(0)
|
||||||
|
const touchStartY = ref(0)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
const currentLocation = computed(() => player.currentLocation)
|
||||||
|
|
||||||
|
// 内容样式
|
||||||
|
const contentStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
width: (containerWidth * zoom.value) + 'px',
|
||||||
|
height: (containerHeight * zoom.value) + 'px',
|
||||||
|
transformOrigin: 'center center'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
function initMap() {
|
||||||
|
const data = getMapData(player.currentLocation)
|
||||||
|
|
||||||
|
// 不再缩放以适应容器,使用原始坐标
|
||||||
|
// 但确保地图足够大
|
||||||
|
const padding = 40
|
||||||
|
|
||||||
|
mapData.value = {
|
||||||
|
nodes: data.nodes.map(n => ({
|
||||||
|
...n,
|
||||||
|
x: n.x,
|
||||||
|
y: n.y
|
||||||
|
})),
|
||||||
|
edges: data.edges,
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
reachable: data.reachable
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到当前节点并居中
|
||||||
|
centerOnLocation()
|
||||||
|
selectedPath.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 居中到当前位置
|
||||||
|
function centerOnLocation() {
|
||||||
|
const currentNode = mapData.value.nodes.find(n => n.id === currentLocation.value)
|
||||||
|
if (!currentNode) return
|
||||||
|
|
||||||
|
currentLocationNode.value = currentNode
|
||||||
|
|
||||||
|
// 计算需要滚动的位置以居中当前节点
|
||||||
|
// 考虑缩放比例
|
||||||
|
const targetLeft = currentNode.x * zoom.value - containerWidth / 2
|
||||||
|
const targetTop = currentNode.y * zoom.value - containerHeight / 2
|
||||||
|
|
||||||
|
scrollLeft.value = Math.max(0, targetLeft)
|
||||||
|
scrollTop.value = Math.max(0, targetTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放控制
|
||||||
|
function zoomIn() {
|
||||||
|
zoom.value = Math.min(2, zoom.value + 0.2)
|
||||||
|
centerOnLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
zoom.value = Math.max(0.5, zoom.value - 0.2)
|
||||||
|
centerOnLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
zoom.value = 1
|
||||||
|
centerOnLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸事件处理(用于拖动)
|
||||||
|
function onTouchStart(e) {
|
||||||
|
touchStartX.value = e.touches[0].clientX
|
||||||
|
touchStartY.value = e.touches[0].clientY
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e) {
|
||||||
|
const deltaX = e.touches[0].clientX - touchStartX.value
|
||||||
|
const deltaY = e.touches[0].clientY - touchStartY.value
|
||||||
|
|
||||||
|
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
||||||
|
isDragging.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd(e) {
|
||||||
|
// 如果不是拖动,则视为点击
|
||||||
|
// 这里的处理在 handleNodeClick 中完成
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动事件
|
||||||
|
function onScroll(e) {
|
||||||
|
// 可以在这里记录滚动位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点样式
|
||||||
|
function getNodeStyle(node) {
|
||||||
|
return {
|
||||||
|
left: (node.x * zoom.value) + 'px',
|
||||||
|
top: (node.y * zoom.value) + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点样式类
|
||||||
|
function getNodeClass(node) {
|
||||||
|
const classes = []
|
||||||
|
|
||||||
|
if (node.id === currentLocation.value) {
|
||||||
|
classes.push('map-node--current')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocked(node)) {
|
||||||
|
classes.push('map-node--locked')
|
||||||
|
} else {
|
||||||
|
if (node.type === 'safe') classes.push('map-node--safe')
|
||||||
|
else if (node.type === 'danger') classes.push('map-node--danger')
|
||||||
|
else if (node.type === 'dungeon') classes.push('map-node--dungeon')
|
||||||
|
|
||||||
|
const dist = getDistance(node)
|
||||||
|
if (dist === 1) classes.push('map-node--adjacent')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查区域是否锁定
|
||||||
|
function isLocked(node) {
|
||||||
|
const loc = LOCATION_CONFIG[node.id]
|
||||||
|
if (!loc?.unlockCondition) return false
|
||||||
|
|
||||||
|
if (loc.unlockCondition.type === 'kill') {
|
||||||
|
const killCount = (player.killCount && player.killCount[loc.unlockCondition.target]) || 0
|
||||||
|
return killCount < loc.unlockCondition.count
|
||||||
|
} else if (loc.unlockCondition.type === 'item') {
|
||||||
|
return !player.inventory.some(i => i.id === loc.unlockCondition.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取距离(步数)
|
||||||
|
function getDistance(node) {
|
||||||
|
return mapData.value.reachable?.[node.id] ?? 99
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取位置名称
|
||||||
|
function getLocationName(id) {
|
||||||
|
return LOCATION_CONFIG[id]?.name || id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取连接线样式
|
||||||
|
function getLineStyle(edge) {
|
||||||
|
const fromNode = mapData.value.nodes.find(n => n.id === edge.from)
|
||||||
|
const toNode = mapData.value.nodes.find(n => n.id === edge.to)
|
||||||
|
|
||||||
|
if (!fromNode || !toNode) return {}
|
||||||
|
|
||||||
|
const dx = (toNode.x - fromNode.x) * zoom.value
|
||||||
|
const dy = (toNode.y - fromNode.y) * zoom.value
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
const angle = Math.atan2(dy, dx) * 180 / Math.PI
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: (fromNode.x * zoom.value) + 'px',
|
||||||
|
top: (fromNode.y * zoom.value) + 'px',
|
||||||
|
width: length + 'px',
|
||||||
|
transform: `rotate(${angle}deg)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查边是否在选中路径上
|
||||||
|
function isInPath(edge) {
|
||||||
|
if (selectedPath.value.length < 2) return false
|
||||||
|
|
||||||
|
for (let i = 0; i < selectedPath.value.length - 1; i++) {
|
||||||
|
const from = selectedPath.value[i]
|
||||||
|
const to = selectedPath.value[i + 1]
|
||||||
|
if ((edge.from === from && edge.to === to) ||
|
||||||
|
(edge.from === to && edge.to === from)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理节点点击
|
||||||
|
function handleNodeClick(node) {
|
||||||
|
if (node.id === currentLocation.value) {
|
||||||
|
selectedPath.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = getPath(currentLocation.value, node.id)
|
||||||
|
if (path) {
|
||||||
|
selectedPath.value = path
|
||||||
|
|
||||||
|
// 如果相邻且未锁定,提示可前往
|
||||||
|
if (path.length === 2 && !isLocked(node)) {
|
||||||
|
game.addLog(`可以前往 ${node.name}`, 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听当前位置变化
|
||||||
|
watch(() => player.currentLocation, () => {
|
||||||
|
initMap()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initMap()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mini-map {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 360rpx;
|
||||||
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2rpx solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scroll {
|
||||||
|
width: 100%;
|
||||||
|
height: 300rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__connections {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-secondary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__path {
|
||||||
|
padding: 10rpx 12rpx;
|
||||||
|
background-color: rgba($accent, 0.15);
|
||||||
|
border: 1rpx solid $accent;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__zoom-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-btn {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba($bg-primary, 0.8);
|
||||||
|
border: 1rpx solid rgba($accent, 0.5);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
backdrop-filter: blur(4rpx);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba($accent, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: $accent;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 2rpx;
|
||||||
|
background-color: rgba(74, 85, 104, 0.5);
|
||||||
|
transform-origin: left center;
|
||||||
|
|
||||||
|
&--path {
|
||||||
|
background-color: rgba($warning, 0.8);
|
||||||
|
height: 3rpx;
|
||||||
|
box-shadow: 0 0 6rpx rgba($warning, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node {
|
||||||
|
position: absolute;
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translate(-50%, -50%) scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--locked {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node__dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid;
|
||||||
|
|
||||||
|
.map-node--safe & {
|
||||||
|
background-color: rgba($success, 0.3);
|
||||||
|
border-color: $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node--danger & {
|
||||||
|
background-color: rgba($danger, 0.3);
|
||||||
|
border-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node--dungeon & {
|
||||||
|
background-color: rgba($accent, 0.3);
|
||||||
|
border-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node--current & {
|
||||||
|
background-color: $warning;
|
||||||
|
border-color: #fff;
|
||||||
|
box-shadow: 0 0 12rpx rgba($warning, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node--locked & {
|
||||||
|
background-color: rgba($text-muted, 0.3);
|
||||||
|
border-color: $text-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node__pulse {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba($warning, 0.4);
|
||||||
|
animation: pulse 1.5s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node--adjacent .node__dot::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
left: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
bottom: -8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1rpx dashed rgba($warning, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node__name {
|
||||||
|
position: absolute;
|
||||||
|
top: 40rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #e2e8f0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.8);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node__lock {
|
||||||
|
position: absolute;
|
||||||
|
top: -12rpx;
|
||||||
|
right: -12rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node__distance {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -12rpx;
|
||||||
|
right: -12rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: rgba($text-secondary, 0.8);
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 2rpx 6rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
background-color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--safe {
|
||||||
|
background-color: $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dungeon {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<view class="progress-bar" :style="{ height }">
|
<view class="progress-bar" :style="{ height }">
|
||||||
<view class="progress-bar__fill" :style="fillStyle"></view>
|
<view class="progress-bar__fill" :style="fillStyle"></view>
|
||||||
<view v-if="showText" class="progress-bar__text">
|
<view v-if="showText" class="progress-bar__text">
|
||||||
{{ value }}/{{ max }}
|
{{ displayValue }}/{{ displayMax }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,6 +22,10 @@ const percentage = computed(() => {
|
|||||||
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
|
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const displayValue = computed(() => Math.floor(props.value))
|
||||||
|
|
||||||
|
const displayMax = computed(() => Math.floor(props.max))
|
||||||
|
|
||||||
const fillStyle = computed(() => ({
|
const fillStyle = computed(() => ({
|
||||||
width: `${percentage.value}%`,
|
width: `${percentage.value}%`,
|
||||||
backgroundColor: props.color
|
backgroundColor: props.color
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<view
|
<view
|
||||||
class="text-button"
|
class="text-button"
|
||||||
:class="[`text-button--${type}`, { 'text-button--disabled': disabled }]"
|
:class="[
|
||||||
|
`text-button--${type}`,
|
||||||
|
{
|
||||||
|
'text-button--disabled': disabled,
|
||||||
|
'text-button--loading': loading
|
||||||
|
}
|
||||||
|
]"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<text>{{ text }}</text>
|
<text v-if="!loading" class="text-button__text">{{ text }}</text>
|
||||||
|
<view v-else class="text-button__spinner"></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -12,13 +19,14 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
type: { type: String, default: 'default' },
|
type: { type: String, default: 'default' },
|
||||||
disabled: { type: Boolean, default: false }
|
disabled: { type: Boolean, default: false },
|
||||||
|
loading: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['click'])
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!props.disabled) {
|
if (!props.disabled && !props.loading) {
|
||||||
emit('click')
|
emit('click')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,43 +34,133 @@ function handleClick() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.text-button {
|
.text-button {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 16rpx 32rpx;
|
padding: 16rpx 32rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
transition: all 0.2s;
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
// 涟漪效果
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.3s, height 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--default {
|
&--default {
|
||||||
background-color: $bg-tertiary;
|
background-color: $bg-tertiary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
|
border: 1rpx solid transparent;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background-color: $accent;
|
||||||
|
color: #fff;
|
||||||
|
border: 1rpx solid $accent;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba($accent, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: darken($accent, 10%);
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 6rpx rgba($accent, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--danger {
|
&--danger {
|
||||||
background-color: rgba($danger, 0.2);
|
background-color: rgba($danger, 0.15);
|
||||||
color: $danger;
|
color: $danger;
|
||||||
|
border: 1rpx solid rgba($danger, 0.3);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: rgba($danger, 0.3);
|
background-color: rgba($danger, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
background-color: rgba($warning, 0.2);
|
background-color: rgba($warning, 0.15);
|
||||||
color: $warning;
|
color: $warning;
|
||||||
|
border: 1rpx solid rgba($warning, 0.3);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: rgba($warning, 0.3);
|
background-color: rgba($warning, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background-color: rgba($success, 0.15);
|
||||||
|
color: $success;
|
||||||
|
border: 1rpx solid rgba($success, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba($success, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
background-color: rgba($info, 0.15);
|
||||||
|
color: $info;
|
||||||
|
border: 1rpx solid rgba($info, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba($info, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--disabled {
|
&--disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,668 @@
|
|||||||
|
<template>
|
||||||
|
<view class="crafting-drawer">
|
||||||
|
<view class="drawer-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="drawer-title">🔨 制造</text>
|
||||||
|
<text class="drawer-subtitle">{{ getSelectedCategoryName() }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="drawer-close" @click="close">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 技能信息显示 -->
|
||||||
|
<view class="crafting-skill-info">
|
||||||
|
<text class="skill-info__label">制造技能等级:</text>
|
||||||
|
<text class="skill-info__level">{{ getCraftingSkillLevel() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<FilterTabs
|
||||||
|
:tabs="categories"
|
||||||
|
:modelValue="selectedCategory"
|
||||||
|
@update:modelValue="selectedCategory = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 配方列表 -->
|
||||||
|
<scroll-view class="recipe-list" scroll-y>
|
||||||
|
<view
|
||||||
|
v-for="recipe in filteredRecipes"
|
||||||
|
:key="recipe.id"
|
||||||
|
class="recipe-item"
|
||||||
|
:class="{ 'recipe-item--disabled': !recipe.canCraftNow }"
|
||||||
|
@click="selectRecipe(recipe)"
|
||||||
|
>
|
||||||
|
<view class="recipe-item__main">
|
||||||
|
<text class="recipe-item__icon">{{ recipe.displayIcon || '📦' }}</text>
|
||||||
|
<view class="recipe-item__info">
|
||||||
|
<text class="recipe-item__name">{{ recipe.displayName }}</text>
|
||||||
|
<text class="recipe-item__desc">{{ getRecipeDescription(recipe) }}</text>
|
||||||
|
<!-- 材料需求 -->
|
||||||
|
<view class="recipe-materials">
|
||||||
|
<text
|
||||||
|
v-for="mat in recipe.materials"
|
||||||
|
:key="mat.itemId"
|
||||||
|
class="material-tag"
|
||||||
|
:class="{ 'material-tag--missing': isMaterialMissing(recipe, mat.itemId) }"
|
||||||
|
>
|
||||||
|
{{ getMaterialIcon(mat.itemId) }} {{ getMaterialName(mat.itemId) }} x{{ mat.count }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="recipe-item__right">
|
||||||
|
<text class="recipe-time">{{ getCraftingTimeDisplay(recipe) }}</text>
|
||||||
|
<text class="recipe-success-rate" :class="getSuccessRateClass(recipe)">
|
||||||
|
{{ getSuccessRateDisplay(recipe) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="filteredRecipes.length === 0" class="recipe-empty">
|
||||||
|
<text class="recipe-empty__text">此分类下没有可制作的配方</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 当前制造进度 -->
|
||||||
|
<view v-if="activeCraftingTask" class="crafting-progress">
|
||||||
|
<view class="progress-header">
|
||||||
|
<text class="progress-title">正在制造...</text>
|
||||||
|
<text class="progress-percent">{{ getCraftingProgress() }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-bar__fill" :style="{ width: getCraftingProgress() + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="progress-item">{{ getCraftingItemName() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配方详情弹窗 -->
|
||||||
|
<view v-if="selectedRecipe" class="recipe-detail-popup" @click="selectedRecipe = null">
|
||||||
|
<view class="recipe-detail" @click.stop>
|
||||||
|
<view class="recipe-detail__header">
|
||||||
|
<text class="recipe-detail__icon">{{ selectedRecipe.displayIcon || '📦' }}</text>
|
||||||
|
<text class="recipe-detail__name">{{ selectedRecipe.displayName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配方属性 -->
|
||||||
|
<view class="recipe-detail__stats">
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">制造时间:</text>
|
||||||
|
<text class="stat-value">{{ getCraftingTimeDisplay(selectedRecipe) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">成功率:</text>
|
||||||
|
<text class="stat-value" :class="getSuccessRateClass(selectedRecipe)">
|
||||||
|
{{ getSuccessRateDisplay(selectedRecipe) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">所需技能:</text>
|
||||||
|
<text class="stat-value">{{ getRequiredSkillDisplay(selectedRecipe) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 材料列表 -->
|
||||||
|
<view class="recipe-detail__materials">
|
||||||
|
<text class="materials-title">所需材料:</text>
|
||||||
|
<view class="materials-list">
|
||||||
|
<view
|
||||||
|
v-for="mat in selectedRecipe.materials"
|
||||||
|
:key="mat.itemId"
|
||||||
|
class="material-item"
|
||||||
|
:class="{ 'material-item--missing': isMaterialMissing(selectedRecipe, mat.itemId) }"
|
||||||
|
>
|
||||||
|
<text class="material-item__icon">{{ getMaterialIcon(mat.itemId) }}</text>
|
||||||
|
<text class="material-item__name">{{ getMaterialName(mat.itemId) }}</text>
|
||||||
|
<text class="material-item__count">{{ mat.count }}</text>
|
||||||
|
<text class="material-item__have">拥有: {{ getMaterialCount(mat.itemId) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预估品质 -->
|
||||||
|
<view class="recipe-detail__quality">
|
||||||
|
<text class="quality-title">预估品质范围:</text>
|
||||||
|
<view class="quality-range">
|
||||||
|
<text class="quality-range__min" :style="{ color: getQualityColor(2) }">普通</text>
|
||||||
|
<text class="quality-range__arrow">→</text>
|
||||||
|
<text class="quality-range__max" :style="{ color: getQualityColor(5) }">史诗</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="recipe-detail__actions">
|
||||||
|
<TextButton
|
||||||
|
text="开始制造"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!selectedRecipe.canCraftNow"
|
||||||
|
@click="startCraftingRecipe"
|
||||||
|
/>
|
||||||
|
<TextButton text="取消" @click="selectedRecipe = null" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { useGameStore } from '@/store/game'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
|
import {
|
||||||
|
getCraftableRecipes,
|
||||||
|
getRecipesByCategory,
|
||||||
|
calculateCraftingTime,
|
||||||
|
calculateSuccessRate,
|
||||||
|
startCrafting,
|
||||||
|
completeCrafting,
|
||||||
|
getAllCategories,
|
||||||
|
getCategoryInfo
|
||||||
|
} from '@/utils/craftingSystem.js'
|
||||||
|
import { getActiveTasks } from '@/utils/taskSystem.js'
|
||||||
|
import { TASK_TYPES } from '@/utils/taskSystem.js'
|
||||||
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
|
|
||||||
|
const selectedCategory = ref('weapon')
|
||||||
|
const selectedRecipe = ref(null)
|
||||||
|
|
||||||
|
// 分类列表
|
||||||
|
const categories = computed(() => {
|
||||||
|
const allCats = getAllCategories()
|
||||||
|
return allCats.map(cat => ({
|
||||||
|
id: cat.id,
|
||||||
|
name: cat.name,
|
||||||
|
icon: cat.icon
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 可制作的配方
|
||||||
|
const craftableRecipes = computed(() => {
|
||||||
|
return getCraftableRecipes(player)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前分类的配方
|
||||||
|
const filteredRecipes = computed(() => {
|
||||||
|
return getRecipesByCategory(player, selectedCategory.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 活动中的制造任务
|
||||||
|
const activeCraftingTask = computed(() => {
|
||||||
|
const activeTasks = getActiveTasks(game)
|
||||||
|
return activeTasks.find(t => t.type === TASK_TYPES.CRAFTING)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取选中分类名称
|
||||||
|
function getSelectedCategoryName() {
|
||||||
|
const cat = getCategoryInfo(selectedCategory.value)
|
||||||
|
return cat.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造技能等级
|
||||||
|
function getCraftingSkillLevel() {
|
||||||
|
const craftingSkill = player.skills.crafting
|
||||||
|
if (!craftingSkill || !craftingSkill.unlocked) {
|
||||||
|
return '未解锁'
|
||||||
|
}
|
||||||
|
return `Lv.${craftingSkill.level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配方描述
|
||||||
|
function getRecipeDescription(recipe) {
|
||||||
|
const item = ITEM_CONFIG[recipe.resultItem]
|
||||||
|
return item?.description || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查材料是否缺失
|
||||||
|
function isMaterialMissing(recipe, materialId) {
|
||||||
|
const missing = recipe.missingMaterials?.find(m => m.itemId === materialId)
|
||||||
|
return !!missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料图标
|
||||||
|
function getMaterialIcon(itemId) {
|
||||||
|
const item = ITEM_CONFIG[itemId]
|
||||||
|
return item?.icon || '📦'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料名称
|
||||||
|
function getMaterialName(itemId) {
|
||||||
|
const item = ITEM_CONFIG[itemId]
|
||||||
|
return item?.name || itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料拥有数量
|
||||||
|
function getMaterialCount(itemId) {
|
||||||
|
const invItem = player.inventory.find(i => i.id === itemId)
|
||||||
|
return invItem?.count || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造时间显示
|
||||||
|
function getCraftingTimeDisplay(recipe) {
|
||||||
|
const time = calculateCraftingTime(player, recipe)
|
||||||
|
if (time >= 60) {
|
||||||
|
return `${Math.floor(time / 60)}分${time % 60}秒`
|
||||||
|
}
|
||||||
|
return `${time}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取成功率显示
|
||||||
|
function getSuccessRateDisplay(recipe) {
|
||||||
|
const rate = calculateSuccessRate(player, recipe)
|
||||||
|
return `${rate}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取成功率样式类
|
||||||
|
function getSuccessRateClass(recipe) {
|
||||||
|
const rate = calculateSuccessRate(player, recipe)
|
||||||
|
if (rate >= 80) return 'success-rate--high'
|
||||||
|
if (rate >= 50) return 'success-rate--medium'
|
||||||
|
return 'success-rate--low'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所需技能显示
|
||||||
|
function getRequiredSkillDisplay(recipe) {
|
||||||
|
const skill = player.skills[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) {
|
||||||
|
return `需要 ${recipe.requiredSkill} Lv.${recipe.requiredSkillLevel}`
|
||||||
|
}
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) {
|
||||||
|
return `${recipe.requiredSkill} Lv.${skill.level}/${recipe.requiredSkillLevel}`
|
||||||
|
}
|
||||||
|
return `${recipe.requiredSkill} Lv.${skill.level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取品质颜色
|
||||||
|
function getQualityColor(quality) {
|
||||||
|
const colors = {
|
||||||
|
1: '#9e9e9e', // 垃圾
|
||||||
|
2: '#ffffff', // 普通
|
||||||
|
3: '#4caf50', // 优秀
|
||||||
|
4: '#2196f3', // 稀有
|
||||||
|
5: '#9c27b0', // 史诗
|
||||||
|
6: '#ff9800' // 传说
|
||||||
|
}
|
||||||
|
return colors[quality] || '#ffffff'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造进度
|
||||||
|
function getCraftingProgress() {
|
||||||
|
if (!activeCraftingTask.value) return 0
|
||||||
|
return Math.floor(activeCraftingTask.value.progress.percentage || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造物品名称
|
||||||
|
function getCraftingItemName() {
|
||||||
|
if (!activeCraftingTask.value) return ''
|
||||||
|
const recipeId = activeCraftingTask.value.data.recipeId
|
||||||
|
const item = ITEM_CONFIG[recipeId]
|
||||||
|
return item?.name || recipeId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择配方
|
||||||
|
function selectRecipe(recipe) {
|
||||||
|
selectedRecipe.value = recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始制造
|
||||||
|
function startCraftingRecipe() {
|
||||||
|
if (!selectedRecipe.value) return
|
||||||
|
|
||||||
|
const result = startCrafting(game, player, selectedRecipe.value.id)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(result.message, 'success')
|
||||||
|
selectedRecipe.value = null
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
function close() {
|
||||||
|
game.closeDrawer()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.crafting-drawer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #16162a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-close {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-skill-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #1e1e36;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info__level {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #252540;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-materials {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #1e1e36;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-tag--missing {
|
||||||
|
color: #f44336;
|
||||||
|
background: #3a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-success-rate {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--high {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--medium {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--low {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-empty {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-empty__text {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-progress {
|
||||||
|
padding: 16px;
|
||||||
|
background: #1e1e36;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar__fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配方详情弹窗
|
||||||
|
.recipe-detail-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 80%;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__stats {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__materials {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materials-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materials-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #252540;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item--missing {
|
||||||
|
background: #3a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__count {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__have {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__quality {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-range__arrow {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,8 +21,9 @@
|
|||||||
>
|
>
|
||||||
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
||||||
<view class="inventory-item__info">
|
<view class="inventory-item__info">
|
||||||
<text class="inventory-item__name" :style="{ color: qualityColor(item.quality) }">
|
<text class="inventory-item__name" :style="{ color: item.qualityColor || '#fff' }">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
|
<text v-if="item.qualityName" class="quality-badge"> [{{ item.qualityName }}]</text>
|
||||||
</text>
|
</text>
|
||||||
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -54,6 +55,12 @@
|
|||||||
type="warning"
|
type="warning"
|
||||||
@click="unequipItem"
|
@click="unequipItem"
|
||||||
/>
|
/>
|
||||||
|
<TextButton
|
||||||
|
v-if="canRead"
|
||||||
|
text="阅读"
|
||||||
|
type="primary"
|
||||||
|
@click="readItem"
|
||||||
|
/>
|
||||||
<TextButton
|
<TextButton
|
||||||
v-if="canUse"
|
v-if="canUse"
|
||||||
text="使用"
|
text="使用"
|
||||||
@@ -73,6 +80,8 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { useGameStore } from '@/store/game'
|
import { useGameStore } from '@/store/game'
|
||||||
|
import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
|
||||||
|
import { startTask } from '@/utils/taskSystem.js'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
@@ -110,6 +119,11 @@ const canUse = computed(() => {
|
|||||||
return selectedItem.value.type === 'consumable'
|
return selectedItem.value.type === 'consumable'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canRead = computed(() => {
|
||||||
|
if (!selectedItem.value) return false
|
||||||
|
return selectedItem.value.type === 'book'
|
||||||
|
})
|
||||||
|
|
||||||
// 装备槽位映射
|
// 装备槽位映射
|
||||||
const slotMap = {
|
const slotMap = {
|
||||||
weapon: 'weapon',
|
weapon: 'weapon',
|
||||||
@@ -124,18 +138,8 @@ function isEquipped(item) {
|
|||||||
const slot = slotMap[item.type]
|
const slot = slotMap[item.type]
|
||||||
if (!slot) return false
|
if (!slot) return false
|
||||||
const equipped = player.equipment[slot]
|
const equipped = player.equipment[slot]
|
||||||
// 通过 uniqueId 或 id 比较
|
// 只通过 uniqueId 比较,确保每个装备独立判断
|
||||||
return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
|
return equipped && equipped.uniqueId === item.uniqueId
|
||||||
}
|
|
||||||
|
|
||||||
function qualityColor(quality) {
|
|
||||||
if (!quality) return '#ffffff'
|
|
||||||
if (quality >= 200) return '#f97316' // 传说
|
|
||||||
if (quality >= 160) return '#a855f7' // 史诗
|
|
||||||
if (quality >= 130) return '#60a5fa' // 稀有
|
|
||||||
if (quality >= 100) return '#4ade80' // 优秀
|
|
||||||
if (quality >= 50) return '#ffffff' // 普通
|
|
||||||
return '#808080' // 垃圾
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -148,30 +152,10 @@ function selectItem(item) {
|
|||||||
|
|
||||||
function equipItem() {
|
function equipItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
const item = selectedItem.value
|
const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
|
||||||
const slot = slotMap[item.type]
|
if (result.success) {
|
||||||
|
|
||||||
if (!slot) return
|
|
||||||
|
|
||||||
// 如果该槽位已有装备,先卸下旧装备放回背包
|
|
||||||
const oldEquip = player.equipment[slot]
|
|
||||||
if (oldEquip) {
|
|
||||||
player.inventory.push(oldEquip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从背包中移除该物品
|
|
||||||
const index = player.inventory.findIndex(i =>
|
|
||||||
(i.uniqueId === item.uniqueId) || (i.id === item.id && i.uniqueId === undefined)
|
|
||||||
)
|
|
||||||
if (index > -1) {
|
|
||||||
player.inventory.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 装备该物品
|
|
||||||
player.equipment[slot] = item
|
|
||||||
|
|
||||||
game.addLog(`装备了 ${item.name}`, 'info')
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unequipItem() {
|
function unequipItem() {
|
||||||
@@ -181,57 +165,42 @@ function unequipItem() {
|
|||||||
|
|
||||||
if (!slot) return
|
if (!slot) return
|
||||||
|
|
||||||
// 从装备槽移除
|
const result = unequipItemBySlot(player, game, slot)
|
||||||
player.equipment[slot] = null
|
if (result.success) {
|
||||||
|
|
||||||
// 放回背包
|
|
||||||
player.inventory.push(item)
|
|
||||||
|
|
||||||
game.addLog(`卸下了 ${item.name}`, 'info')
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useItem() {
|
function useItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
const item = selectedItem.value
|
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
||||||
|
if (result.success) {
|
||||||
// 检查是否是消耗品
|
|
||||||
if (item.type !== 'consumable') return
|
|
||||||
|
|
||||||
// 应用效果
|
|
||||||
if (item.effect) {
|
|
||||||
if (item.effect.health) {
|
|
||||||
player.currentStats.health = Math.min(
|
|
||||||
player.currentStats.maxHealth,
|
|
||||||
player.currentStats.health + item.effect.health
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (item.effect.stamina) {
|
|
||||||
player.currentStats.stamina = Math.min(
|
|
||||||
player.currentStats.maxStamina,
|
|
||||||
player.currentStats.stamina + item.effect.stamina
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (item.effect.sanity) {
|
|
||||||
player.currentStats.sanity = Math.min(
|
|
||||||
player.currentStats.maxSanity,
|
|
||||||
player.currentStats.sanity + item.effect.sanity
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从背包中移除(如果是堆叠物品,减少数量)
|
|
||||||
if (item.count > 1) {
|
|
||||||
item.count--
|
|
||||||
} else {
|
|
||||||
const index = player.inventory.findIndex(i => i.id === item.id)
|
|
||||||
if (index > -1) {
|
|
||||||
player.inventory.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game.addLog(`使用了 ${item.name}`, 'info')
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readItem() {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
// 检查是否已经在阅读中
|
||||||
|
const existingTask = game.activeTasks?.find(t => t.type === 'reading')
|
||||||
|
if (existingTask) {
|
||||||
|
game.addLog('已经在阅读其他书籍了', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = startTask(game, player, 'reading', {
|
||||||
|
itemId: selectedItem.value.id,
|
||||||
|
duration: selectedItem.value.readingTime || 60
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始阅读《${selectedItem.value.name}》`, 'info')
|
||||||
|
selectedItem.value = null
|
||||||
|
emit('close') // 关闭背包以便看到阅读进度
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sellItem() {
|
function sellItem() {
|
||||||
@@ -337,6 +306,12 @@ function sellItem() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality-badge {
|
||||||
|
font-size: 18rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.inventory-empty {
|
.inventory-empty {
|
||||||
padding: 80rpx;
|
padding: 80rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -130,6 +130,34 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 数量选择弹窗 -->
|
||||||
|
<view v-if="quantitySelector" class="item-detail-popup" @click="cancelQuantitySell">
|
||||||
|
<view class="item-detail" @click.stop>
|
||||||
|
<text class="item-detail__name">选择出售数量</text>
|
||||||
|
<view class="quantity-selector">
|
||||||
|
<text class="quantity-selector__item-name">{{ quantitySelector.item.icon }} {{ quantitySelector.item.name }}</text>
|
||||||
|
<text class="quantity-selector__available">拥有: {{ quantitySelector.maxCount }}</text>
|
||||||
|
|
||||||
|
<view class="quantity-selector__controls">
|
||||||
|
<TextButton text="-" type="default" @click="decreaseSellCount" :disabled="quantitySelector.selectedCount <= 1" />
|
||||||
|
<text class="quantity-selector__count">{{ quantitySelector.selectedCount }}</text>
|
||||||
|
<TextButton text="+" type="default" @click="increaseSellCount" :disabled="quantitySelector.selectedCount >= quantitySelector.maxCount" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<TextButton text="全部" type="default" size="small" @click="setMaxSellCount" style="margin-top: 12rpx;" />
|
||||||
|
|
||||||
|
<view class="quantity-selector__preview">
|
||||||
|
<text class="quantity-selector__price-label">预计获得:</text>
|
||||||
|
<text class="quantity-selector__price">{{ getSellPrice(quantitySelector.item) * quantitySelector.selectedCount }} 铜币</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-detail__actions">
|
||||||
|
<TextButton text="确认出售" type="primary" @click="confirmQuantitySell" />
|
||||||
|
<TextButton text="取消" @click="cancelQuantitySell" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -138,6 +166,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { useGameStore } from '@/store/game'
|
import { useGameStore } from '@/store/game'
|
||||||
import { getShopConfig, getBuyPrice as calcBuyPrice, getSellPrice as calcSellPrice } from '@/config/shop.js'
|
import { getShopConfig, getBuyPrice as calcBuyPrice, getSellPrice as calcSellPrice } from '@/config/shop.js'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
import { addItemToInventory, removeItemFromInventory } from '@/utils/itemSystem.js'
|
import { addItemToInventory, removeItemFromInventory } from '@/utils/itemSystem.js'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
@@ -149,6 +178,8 @@ const currentMode = ref('buy')
|
|||||||
const selectedItem = ref(null)
|
const selectedItem = ref(null)
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const selectedItems = ref([])
|
const selectedItems = ref([])
|
||||||
|
// 数量选择弹窗状态
|
||||||
|
const quantitySelector = ref(null) // { item: Object, maxCount: Number, selectedCount: Number }
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -171,10 +202,18 @@ const shopItems = computed(() => {
|
|||||||
return config.items
|
return config.items
|
||||||
.filter(item => item.stock === -1 || item.stock > 0)
|
.filter(item => item.stock === -1 || item.stock > 0)
|
||||||
.map(shopItem => {
|
.map(shopItem => {
|
||||||
const itemConfig = { ...shopItem }
|
// 从 ITEM_CONFIG 获取完整物品信息
|
||||||
itemConfig.id = shopItem.itemId
|
const itemConfig = ITEM_CONFIG[shopItem.itemId]
|
||||||
return itemConfig
|
if (!itemConfig) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
...itemConfig, // 复制所有物品属性(name, icon, description等)
|
||||||
|
id: shopItem.itemId, // 使用 itemId 作为 id
|
||||||
|
stock: shopItem.stock,
|
||||||
|
baseStock: shopItem.baseStock
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 玩家可出售物品
|
// 玩家可出售物品
|
||||||
@@ -248,7 +287,7 @@ function isItemSelected(item) {
|
|||||||
// 处理出售列表点击
|
// 处理出售列表点击
|
||||||
function handleSellItemClick(item) {
|
function handleSellItemClick(item) {
|
||||||
if (bulkMode.value) {
|
if (bulkMode.value) {
|
||||||
// 批量模式:切换选中状态
|
// 批量模式:切换选中状态(批量出售出售全部)
|
||||||
const index = selectedItems.value.findIndex(i =>
|
const index = selectedItems.value.findIndex(i =>
|
||||||
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
|
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
|
||||||
)
|
)
|
||||||
@@ -258,9 +297,19 @@ function handleSellItemClick(item) {
|
|||||||
selectedItems.value.push(item)
|
selectedItems.value.push(item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 普通模式:显示详情
|
// 普通模式:检查是否是堆叠物品
|
||||||
|
if (item.count > 1) {
|
||||||
|
// 堆叠物品:显示数量选择弹窗
|
||||||
|
quantitySelector.value = {
|
||||||
|
item: item,
|
||||||
|
maxCount: item.count,
|
||||||
|
selectedCount: 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非堆叠物品:直接显示详情
|
||||||
trySell(item)
|
trySell(item)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全选
|
// 全选
|
||||||
@@ -338,20 +387,68 @@ function confirmSell() {
|
|||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
const item = selectedItem.value
|
const item = selectedItem.value
|
||||||
const price = getSellPrice(item)
|
const sellCount = item.sellCount || 1
|
||||||
|
const price = getSellPrice(item) * sellCount
|
||||||
|
|
||||||
// 移除物品
|
// 移除物品
|
||||||
const result = removeItemFromInventory(player, item.uniqueId || item.id)
|
const result = removeItemFromInventory(player, item.uniqueId || item.id, sellCount)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 增加金币
|
// 增加金币
|
||||||
player.currency.copper += price
|
player.currency.copper += price
|
||||||
game.addLog(`出售了 ${item.name},获得 ${price} 铜币`, 'reward')
|
const countText = sellCount > 1 ? `x${sellCount}` : ''
|
||||||
|
game.addLog(`出售了 ${item.name}${countText},获得 ${price} 铜币`, 'reward')
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确认数量选择出售
|
||||||
|
function confirmQuantitySell() {
|
||||||
|
if (!quantitySelector.value) return
|
||||||
|
|
||||||
|
const { item, selectedCount } = quantitySelector.value
|
||||||
|
const price = getSellPrice(item) * selectedCount
|
||||||
|
|
||||||
|
// 移除指定数量的物品
|
||||||
|
const result = removeItemFromInventory(player, item.uniqueId || item.id, selectedCount)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 增加金币
|
||||||
|
player.currency.copper += price
|
||||||
|
game.addLog(`出售了 ${item.name}x${selectedCount},获得 ${price} 铜币`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
quantitySelector.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消数量选择
|
||||||
|
function cancelQuantitySell() {
|
||||||
|
quantitySelector.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加出售数量
|
||||||
|
function increaseSellCount() {
|
||||||
|
if (quantitySelector.value && quantitySelector.value.selectedCount < quantitySelector.value.maxCount) {
|
||||||
|
quantitySelector.value.selectedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少出售数量
|
||||||
|
function decreaseSellCount() {
|
||||||
|
if (quantitySelector.value && quantitySelector.value.selectedCount > 1) {
|
||||||
|
quantitySelector.value.selectedCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置最大出售数量
|
||||||
|
function setMaxSellCount() {
|
||||||
|
if (quantitySelector.value) {
|
||||||
|
quantitySelector.value.selectedCount = quantitySelector.value.maxCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
@@ -630,4 +727,58 @@ function close() {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数量选择器样式
|
||||||
|
.quantity-selector {
|
||||||
|
&__item-name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__available {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
min-width: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price-label {
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="visible" class="drawer" @click="handleMaskClick">
|
<view
|
||||||
|
v-if="visible"
|
||||||
|
class="drawer"
|
||||||
|
:class="{ 'drawer--visible': visible }"
|
||||||
|
@click="handleMaskClick"
|
||||||
|
>
|
||||||
<view class="drawer__content" :style="{ width }" @click.stop>
|
<view class="drawer__content" :style="{ width }" @click.stop>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</view>
|
</view>
|
||||||
@@ -9,7 +14,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
width: { type: String, default: '600rpx' }
|
width: { type: String, default: '600rpx' },
|
||||||
|
position: { type: String, default: 'right' } // right, left, top, bottom
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
@@ -26,9 +32,20 @@ function handleMaskClick() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0,0,0,0.6);
|
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -36,14 +53,70 @@ function handleMaskClick() {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
border-left: 1rpx solid $border-color;
|
border-left: 1rpx solid $border-color;
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-shadow: -4rpx 0 20rpx rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideInRight 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
// 不同的位置动画
|
||||||
from { transform: translateX(100%); }
|
.drawer--left {
|
||||||
to { transform: translateX(0); }
|
.drawer__content {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: 1rpx solid $border-color;
|
||||||
|
animation: slideInLeft 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
box-shadow: 4rpx 0 20rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭动画 (需要配合Vue transition组件实现更复杂的动画)
|
||||||
|
.drawer.drawer--closing {
|
||||||
|
&::before {
|
||||||
|
animation: fadeOut 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer__content {
|
||||||
|
animation: slideOutRight 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:class="`log-item--${log.type}`"
|
:class="`log-item--${log.type}`"
|
||||||
:id="'log-' + log.id"
|
:id="'log-' + log.id"
|
||||||
>
|
>
|
||||||
|
<text class="log-item__icon">{{ getLogIcon(log.type) }}</text>
|
||||||
<text class="log-item__time">[{{ log.time }}]</text>
|
<text class="log-item__time">[{{ log.time }}]</text>
|
||||||
<text class="log-item__message">{{ log.message }}</text>
|
<text class="log-item__message">{{ log.message }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -45,52 +46,88 @@ watch(lastLogId, (newId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 获取日志类型图标
|
||||||
|
function getLogIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
combat: '⚔️',
|
||||||
|
system: '🔔',
|
||||||
|
reward: '🎁',
|
||||||
|
info: 'ℹ️',
|
||||||
|
warning: '⚠️',
|
||||||
|
error: '❌',
|
||||||
|
success: '✅',
|
||||||
|
story: '📖'
|
||||||
|
}
|
||||||
|
return icons[type] || '•'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.log-panel {
|
.log-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 16rpx;
|
padding: 12rpx 16rpx;
|
||||||
background-color: $bg-primary;
|
background-color: $bg-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-item {
|
.log-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: flex-start;
|
||||||
padding: 8rpx 0;
|
padding: 8rpx 12rpx;
|
||||||
border-bottom: 1rpx solid $bg-tertiary;
|
margin-bottom: 4rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: $bg-secondary;
|
||||||
|
border-left: 3rpx solid transparent;
|
||||||
|
animation: slideIn 0.2s ease;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__time {
|
&__time {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-size: 22rpx;
|
font-size: 20rpx;
|
||||||
margin-right: 8rpx;
|
margin-right: 8rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不同类型的日志样式
|
// 不同类型的日志样式
|
||||||
&--combat {
|
&--combat {
|
||||||
|
background-color: rgba($danger, 0.1);
|
||||||
|
border-left-color: $danger;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--system {
|
&--system {
|
||||||
|
background-color: rgba($accent, 0.1);
|
||||||
|
border-left-color: $accent;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--reward {
|
&--reward {
|
||||||
|
background-color: rgba($warning, 0.1);
|
||||||
|
border-left-color: $warning;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,25 +138,55 @@ watch(lastLogId, (newId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
|
background-color: rgba($warning, 0.1);
|
||||||
|
border-left-color: $warning;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
|
background-color: rgba($danger, 0.15);
|
||||||
|
border-left-color: $danger;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--success {
|
&--success {
|
||||||
|
background-color: rgba($success, 0.1);
|
||||||
|
border-left-color: $success;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--story {
|
||||||
|
background-color: rgba($info, 0.1);
|
||||||
|
border-left-color: $info;
|
||||||
|
|
||||||
|
.log-item__message {
|
||||||
|
color: $info;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-anchor {
|
.log-anchor {
|
||||||
height: 1rpx;
|
height: 1rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+108
-30
@@ -14,6 +14,11 @@
|
|||||||
<text class="description-text">{{ currentLocation.description }}</text>
|
<text class="description-text">{{ currentLocation.description }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 迷你地图 -->
|
||||||
|
<view class="map-panel__minimap">
|
||||||
|
<MiniMap />
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 战斗状态 -->
|
<!-- 战斗状态 -->
|
||||||
<view v-if="game.inCombat" class="combat-status">
|
<view v-if="game.inCombat" class="combat-status">
|
||||||
<text class="combat-status__title">⚔️ 战斗中</text>
|
<text class="combat-status__title">⚔️ 战斗中</text>
|
||||||
@@ -32,6 +37,17 @@
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 寻找敌人中状态 -->
|
||||||
|
<view v-if="game.isSearching && !game.inCombat" class="searching-status">
|
||||||
|
<text class="searching-status__title">🔍 寻找中...</text>
|
||||||
|
<text class="searching-status__desc">正在寻找新的敌人</text>
|
||||||
|
<view class="searching-status__dots">
|
||||||
|
<text class="dot">●</text>
|
||||||
|
<text class="dot">●</text>
|
||||||
|
<text class="dot">●</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 区域列表 -->
|
<!-- 区域列表 -->
|
||||||
<view class="map-panel__locations-header">
|
<view class="map-panel__locations-header">
|
||||||
<text class="locations-title">可前往的区域</text>
|
<text class="locations-title">可前往的区域</text>
|
||||||
@@ -105,12 +121,14 @@ import { useGameStore } from '@/store/game'
|
|||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { LOCATION_CONFIG } from '@/config/locations'
|
import { LOCATION_CONFIG } from '@/config/locations'
|
||||||
import { NPC_CONFIG } from '@/config/npcs.js'
|
import { NPC_CONFIG } from '@/config/npcs.js'
|
||||||
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
import { ENEMY_CONFIG, getRandomEnemyForLocation } from '@/config/enemies.js'
|
||||||
import { getShopConfig } from '@/config/shop.js'
|
import { getShopConfig } from '@/config/shop.js'
|
||||||
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
||||||
|
import { triggerExploreEvent, tryFlee, startNPCDialogue } from '@/utils/eventSystem.js'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
|
import MiniMap from '@/components/common/MiniMap.vue'
|
||||||
|
|
||||||
const game = useGameStore()
|
const game = useGameStore()
|
||||||
const player = usePlayerStore()
|
const player = usePlayerStore()
|
||||||
@@ -151,10 +169,10 @@ const availableLocations = computed(() => {
|
|||||||
// 检查解锁条件
|
// 检查解锁条件
|
||||||
if (loc.unlockCondition) {
|
if (loc.unlockCondition) {
|
||||||
if (loc.unlockCondition.type === 'kill') {
|
if (loc.unlockCondition.type === 'kill') {
|
||||||
// 检查击杀条件(需要追踪击杀数)
|
// 检查击杀条件(使用统一的killCount存储)
|
||||||
const killCount = player.flags[`kill_${loc.unlockCondition.target}`] || 0
|
const killCount = (player.killCount && player.killCount[loc.unlockCondition.target]) || 0
|
||||||
locked = killCount < loc.unlockCondition.count
|
locked = killCount < loc.unlockCondition.count
|
||||||
lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)}`
|
lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)} (当前: ${killCount})`
|
||||||
} else if (loc.unlockCondition.type === 'item') {
|
} else if (loc.unlockCondition.type === 'item') {
|
||||||
// 检查物品条件
|
// 检查物品条件
|
||||||
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.item)
|
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.item)
|
||||||
@@ -191,7 +209,8 @@ const currentActions = computed(() => {
|
|||||||
trade: { id: 'trade', label: '交易', type: 'default' },
|
trade: { id: 'trade', label: '交易', type: 'default' },
|
||||||
explore: { id: 'explore', label: '探索', type: 'warning' },
|
explore: { id: 'explore', label: '探索', type: 'warning' },
|
||||||
combat: { id: 'combat', label: '战斗', type: 'danger' },
|
combat: { id: 'combat', label: '战斗', type: 'danger' },
|
||||||
read: { id: 'read', label: '阅读', type: 'default' }
|
read: { id: 'read', label: '阅读', type: 'default' },
|
||||||
|
crafting: { id: 'crafting', label: '制造', type: 'primary' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 战斗中只显示战斗相关
|
// 战斗中只显示战斗相关
|
||||||
@@ -233,16 +252,8 @@ const availableNPCs = computed(() => {
|
|||||||
function talkToNPC(npc) {
|
function talkToNPC(npc) {
|
||||||
game.addLog(`与 ${npc.name} 开始对话...`, 'info')
|
game.addLog(`与 ${npc.name} 开始对话...`, 'info')
|
||||||
|
|
||||||
// 打开 NPC 对话
|
// 使用 eventSystem 的 startNPCDialogue 来处理对话
|
||||||
if (npc.dialogue && npc.dialogue.first) {
|
startNPCDialogue(game, npc.id, 'first', player)
|
||||||
game.drawerState.event = true
|
|
||||||
game.currentEvent = {
|
|
||||||
type: 'npc_dialogue',
|
|
||||||
npcId: npc.id,
|
|
||||||
dialogue: npc.dialogue.first,
|
|
||||||
choices: npc.dialogue.first.choices || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openInventory() {
|
function openInventory() {
|
||||||
@@ -302,33 +313,37 @@ function doAction(actionId) {
|
|||||||
break
|
break
|
||||||
case 'explore':
|
case 'explore':
|
||||||
game.addLog('开始探索...', 'info')
|
game.addLog('开始探索...', 'info')
|
||||||
// 探索逻辑
|
const exploreResult = triggerExploreEvent(game, player)
|
||||||
|
if (!exploreResult.success) {
|
||||||
|
game.addLog(exploreResult.message, 'info')
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'combat':
|
case 'combat':
|
||||||
startCombat()
|
startCombat()
|
||||||
break
|
break
|
||||||
case 'flee':
|
case 'flee':
|
||||||
game.addLog('尝试逃跑...', 'combat')
|
game.addLog('尝试逃跑...', 'combat')
|
||||||
// 逃跑逻辑
|
const fleeResult = tryFlee(game, player)
|
||||||
|
if (!fleeResult.success) {
|
||||||
|
// 逃跑失败,玩家会多受到一次攻击(在战斗循环中处理)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'read':
|
case 'read':
|
||||||
game.addLog('找个安静的地方阅读...', 'info')
|
game.addLog('找个安静的地方阅读...', 'info')
|
||||||
break
|
break
|
||||||
|
case 'crafting':
|
||||||
|
game.drawerState.crafting = true
|
||||||
|
game.addLog('打开制造界面...', 'info')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCombat() {
|
function startCombat() {
|
||||||
const current = LOCATION_CONFIG[player.currentLocation]
|
// 使用新的敌人获取函数
|
||||||
if (!current || !current.enemies || current.enemies.length === 0) {
|
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||||
game.addLog('这里没有敌人', 'info')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const enemyId = current.enemies[0]
|
|
||||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
|
||||||
|
|
||||||
if (!enemyConfig) {
|
if (!enemyConfig) {
|
||||||
game.addLog('敌人配置错误', 'error')
|
game.addLog('这里没有敌人', 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,14 +352,15 @@ function startCombat() {
|
|||||||
// 获取环境类型
|
// 获取环境类型
|
||||||
const environment = getEnvironmentType(player.currentLocation)
|
const environment = getEnvironmentType(player.currentLocation)
|
||||||
|
|
||||||
// 使用 initCombat 初始化战斗(包含 expReward 和 drops)
|
// 使用 initCombat 初始化战斗,传入偏好的战斗姿态
|
||||||
game.combatState = initCombat(enemyId, enemyConfig, environment)
|
game.combatState = initCombat(enemyConfig.id, enemyConfig, environment, game.preferredStance)
|
||||||
game.inCombat = true
|
game.inCombat = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeStance(stance) {
|
function changeStance(stance) {
|
||||||
if (game.combatState) {
|
if (game.combatState) {
|
||||||
game.combatState.stance = stance
|
game.combatState.stance = stance
|
||||||
|
game.preferredStance = stance // 记住玩家选择的姿态
|
||||||
game.addLog(`切换到${stanceTabs.find(t => t.id === stance)?.label}姿态`, 'combat')
|
game.addLog(`切换到${stanceTabs.find(t => t.id === stance)?.label}姿态`, 'combat')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,12 +384,14 @@ function toggleAutoCombat() {
|
|||||||
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
|
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
|
||||||
// 如果当前不在战斗中且在危险区域,立即开始战斗
|
// 如果当前不在战斗中且在危险区域,立即开始战斗
|
||||||
if (!game.inCombat) {
|
if (!game.inCombat) {
|
||||||
const current = LOCATION_CONFIG[player.currentLocation]
|
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||||
if (current && current.enemies && current.enemies.length > 0) {
|
if (enemyConfig) {
|
||||||
startCombat()
|
startCombat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 关闭自动战斗时,清除寻找中状态
|
||||||
|
game.isSearching = false
|
||||||
game.addLog('自动战斗已关闭', 'info')
|
game.addLog('自动战斗已关闭', 'info')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,6 +423,10 @@ function toggleAutoCombat() {
|
|||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__minimap {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
&__locations-header {
|
&__locations-header {
|
||||||
padding: 12rpx 16rpx 8rpx;
|
padding: 12rpx 16rpx 8rpx;
|
||||||
}
|
}
|
||||||
@@ -466,6 +488,62 @@ function toggleAutoCombat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 寻找中状态样式
|
||||||
|
.searching-status {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: rgba($accent, 0.1);
|
||||||
|
border: 1rpx solid $accent;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 24rpx;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.locations-title {
|
.locations-title {
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
|||||||
@@ -73,30 +73,58 @@
|
|||||||
<view class="equipment-slots">
|
<view class="equipment-slots">
|
||||||
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
|
||||||
<text class="equipment-slot__label">武器</text>
|
<text class="equipment-slot__label">武器</text>
|
||||||
<text v-if="player.equipment.weapon" class="equipment-slot__item">
|
<view v-if="player.equipment.weapon" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" :style="{ color: player.equipment.weapon.qualityColor }">
|
||||||
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
|
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
|
||||||
</text>
|
</text>
|
||||||
|
<text class="equipment-slot__stats">攻{{ player.equipment.weapon.finalDamage }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.weapon.qualityName }}]</text>
|
||||||
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
|
||||||
<text class="equipment-slot__label">防具</text>
|
<text class="equipment-slot__label">防具</text>
|
||||||
<text v-if="player.equipment.armor" class="equipment-slot__item">
|
<view v-if="player.equipment.armor" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" :style="{ color: player.equipment.armor.qualityColor }">
|
||||||
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
|
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
|
||||||
</text>
|
</text>
|
||||||
|
<text class="equipment-slot__stats">防{{ player.equipment.armor.finalDefense }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.armor.qualityName }}]</text>
|
||||||
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
|
||||||
<text class="equipment-slot__label">盾牌</text>
|
<text class="equipment-slot__label">盾牌</text>
|
||||||
<text v-if="player.equipment.shield" class="equipment-slot__item">
|
<view v-if="player.equipment.shield" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" :style="{ color: player.equipment.shield.qualityColor }">
|
||||||
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
|
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
|
||||||
</text>
|
</text>
|
||||||
|
<text class="equipment-slot__stats">格挡{{ player.equipment.shield.finalShield }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.shield.qualityName }}]</text>
|
||||||
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
|
||||||
<text class="equipment-slot__label">饰品</text>
|
<text class="equipment-slot__label">饰品</text>
|
||||||
<text v-if="player.equipment.accessory" class="equipment-slot__item">
|
<view v-if="player.equipment.accessory" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" :style="{ color: player.equipment.accessory.qualityColor }">
|
||||||
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
|
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
|
||||||
</text>
|
</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.accessory.qualityName }}]</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
|
</view>
|
||||||
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.prosthetic }">
|
||||||
|
<text class="equipment-slot__label">义体</text>
|
||||||
|
<view v-if="player.equipment.prosthetic" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" style="color: #ff6b9d;">
|
||||||
|
{{ player.equipment.prosthetic.icon }} {{ player.equipment.prosthetic.name }}
|
||||||
|
</text>
|
||||||
|
<text v-if="player.equipment.prosthetic.stats" class="equipment-slot__stats">
|
||||||
|
{{ formatProstheticStats(player.equipment.prosthetic.stats) }}
|
||||||
|
</text>
|
||||||
|
<text v-if="player.equipment.prosthetic.skill" class="equipment-slot__quality">[{{ getProstheticSkillName(player.equipment.prosthetic.skill) }}]</text>
|
||||||
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -127,10 +155,11 @@
|
|||||||
<!-- 技能列表 -->
|
<!-- 技能列表 -->
|
||||||
<scroll-view class="status-panel__skills" scroll-y>
|
<scroll-view class="status-panel__skills" scroll-y>
|
||||||
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
|
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
|
||||||
|
<view class="skill-item__main" @click="toggleSkillDetail(skill.id)">
|
||||||
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
|
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
|
||||||
<view class="skill-item__info">
|
<view class="skill-item__info">
|
||||||
<text class="skill-item__name">{{ skill.name }}</text>
|
<text class="skill-item__name">{{ skill.name }}</text>
|
||||||
<text class="skill-item__level">Lv.{{ skill.level }}</text>
|
<text class="skill-item__level">Lv.{{ skill.level }}/{{ skill.maxLevel }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="skill-item__bar">
|
<view class="skill-item__bar">
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -139,27 +168,172 @@
|
|||||||
height="8rpx"
|
height="8rpx"
|
||||||
:showText="false"
|
:showText="false"
|
||||||
/>
|
/>
|
||||||
|
<text class="skill-item__progress">{{ Math.floor(skill.exp) }}/{{ Math.floor(skill.maxExp) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="skill-item__expand">{{ expandedSkills.includes(skill.id) ? '▼' : '▶' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 技能详情(展开后显示) -->
|
||||||
|
<view v-if="expandedSkills.includes(skill.id)" class="skill-item__detail">
|
||||||
|
<!-- 已获得的增幅 -->
|
||||||
|
<view v-if="getActiveMilestones(skill).length > 0" class="skill-detail__section">
|
||||||
|
<text class="skill-detail__title">✓ 已获得增幅</text>
|
||||||
|
<view v-for="m in getActiveMilestones(skill)" :key="m.level" class="skill-detail__bonus">
|
||||||
|
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
|
||||||
|
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 即将获得的增幅 -->
|
||||||
|
<view v-if="getUpcomingMilestones(skill).length > 0" class="skill-detail__section">
|
||||||
|
<text class="skill-detail__title">即将解锁</text>
|
||||||
|
<view v-for="m in getUpcomingMilestones(skill)" :key="m.level" class="skill-detail__bonus upcoming">
|
||||||
|
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
|
||||||
|
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="getActiveMilestones(skill).length === 0 && getUpcomingMilestones(skill).length === 0" class="skill-detail__empty">
|
||||||
|
<text class="skill-detail__empty-text">暂无增幅效果</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
||||||
<text class="skill-empty__text">暂无技能</text>
|
<text class="skill-empty__text">暂无技能</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 书籍区域 -->
|
||||||
|
<Collapse title="📚 书籍" :expanded="showBooks" @toggle="showBooks = $event">
|
||||||
|
<!-- 活动中的阅读任务 -->
|
||||||
|
<view v-if="readingTask" class="reading-task">
|
||||||
|
<text class="reading-task__title">正在阅读:{{ readingTask.bookName }}</text>
|
||||||
|
<ProgressBar
|
||||||
|
:value="readingTask.progress"
|
||||||
|
:max="readingTask.totalTime"
|
||||||
|
color="#4ecdc4"
|
||||||
|
:showText="true"
|
||||||
|
height="16rpx"
|
||||||
|
/>
|
||||||
|
<text class="reading-task__time">{{ readingTimeText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可阅读的书籍列表 -->
|
||||||
|
<view v-if="!readingTask && ownedBooks.length > 0" class="books-list">
|
||||||
|
<view
|
||||||
|
v-for="book in ownedBooks"
|
||||||
|
:key="book.id"
|
||||||
|
class="book-item"
|
||||||
|
@click="startReading(book)"
|
||||||
|
>
|
||||||
|
<text class="book-item__icon">{{ book.icon || '📖' }}</text>
|
||||||
|
<view class="book-item__info">
|
||||||
|
<text class="book-item__name">{{ book.name }}</text>
|
||||||
|
<text class="book-item__desc">阅读需 {{ book.readingTime }}秒</text>
|
||||||
|
</view>
|
||||||
|
<TextButton text="阅读" type="primary" size="small" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!readingTask && ownedBooks.length === 0" class="books-empty">
|
||||||
|
<text class="books-empty__text">暂无书籍,去商店看看吧</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<!-- 训练区域 -->
|
||||||
|
<Collapse title="⚔️ 训练" :expanded="showTraining" @toggle="showTraining = $event">
|
||||||
|
<!-- 活动中的训练任务 -->
|
||||||
|
<view v-if="trainingTask" class="training-task">
|
||||||
|
<text class="training-task__title">正在训练:{{ trainingTask.skillName }}</text>
|
||||||
|
<ProgressBar
|
||||||
|
:value="trainingTask.progress"
|
||||||
|
:max="trainingTask.totalTime || 300"
|
||||||
|
color="#ff6b6b"
|
||||||
|
:showText="true"
|
||||||
|
height="16rpx"
|
||||||
|
/>
|
||||||
|
<TextButton text="停止训练" type="warning" size="small" @click="stopTraining" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可训练的技能列表 -->
|
||||||
|
<view v-if="!trainingTask && trainableSkills.length > 0" class="trainable-list">
|
||||||
|
<view
|
||||||
|
v-for="skill in trainableSkills"
|
||||||
|
:key="skill.id"
|
||||||
|
class="trainable-item"
|
||||||
|
>
|
||||||
|
<text class="trainable-item__icon">{{ skill.icon || '⚔️' }}</text>
|
||||||
|
<view class="trainable-item__info">
|
||||||
|
<text class="trainable-item__name">{{ skill.name }}</text>
|
||||||
|
<text class="trainable-item__level">Lv.{{ skill.level }}</text>
|
||||||
|
</view>
|
||||||
|
<TextButton
|
||||||
|
text="训练"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="player.currentStats.stamina < 10"
|
||||||
|
@click="startTraining(skill)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!trainingTask && trainableSkills.length === 0" class="training-empty">
|
||||||
|
<text class="training-empty__text">暂无可训练技能</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<!-- 活动任务面板 -->
|
||||||
|
<Collapse title="📋 活动任务" :expanded="showTasks" @toggle="showTasks = $event">
|
||||||
|
<view v-if="activeTasksList.length > 0" class="active-tasks-list">
|
||||||
|
<view v-for="task in activeTasksList" :key="task.id" class="active-task-item">
|
||||||
|
<view class="active-task-item__header">
|
||||||
|
<text class="active-task-item__name">{{ task.name }}</text>
|
||||||
|
<TextButton
|
||||||
|
text="取消"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="cancelActiveTask(task)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<ProgressBar
|
||||||
|
:value="task.progress"
|
||||||
|
:max="task.total || 100"
|
||||||
|
color="#4ecdc4"
|
||||||
|
:showText="true"
|
||||||
|
height="12rpx"
|
||||||
|
/>
|
||||||
|
<text class="active-task-item__time">{{ task.timeText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="tasks-empty">
|
||||||
|
<text class="tasks-empty__text">暂无活动任务</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { useGameStore } from '@/store/game'
|
||||||
import { SKILL_CONFIG } from '@/config/skills'
|
import { SKILL_CONFIG } from '@/config/skills'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items'
|
||||||
|
import { startTask, endTask } from '@/utils/taskSystem'
|
||||||
|
import { getSkillDisplayInfo } from '@/utils/skillSystem'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import StatItem from '@/components/common/StatItem.vue'
|
import StatItem from '@/components/common/StatItem.vue'
|
||||||
import Collapse from '@/components/common/Collapse.vue'
|
import Collapse from '@/components/common/Collapse.vue'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
const player = usePlayerStore()
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
const showMoreStats = ref(false)
|
const showMoreStats = ref(false)
|
||||||
const currentSkillFilter = ref('all')
|
const currentSkillFilter = ref('all')
|
||||||
|
const showBooks = ref(false)
|
||||||
|
const showTraining = ref(false)
|
||||||
|
const showTasks = ref(true)
|
||||||
|
const expandedSkills = ref([]) // 展开的技能ID列表
|
||||||
|
|
||||||
const skillFilterTabs = [
|
const skillFilterTabs = [
|
||||||
{ id: 'all', label: '全部' },
|
{ id: 'all', label: '全部' },
|
||||||
@@ -183,15 +357,8 @@ const currencyText = computed(() => {
|
|||||||
const filteredSkills = computed(() => {
|
const filteredSkills = computed(() => {
|
||||||
const skills = Object.entries(player.skills || {})
|
const skills = Object.entries(player.skills || {})
|
||||||
.filter(([_, skill]) => skill.unlocked)
|
.filter(([_, skill]) => skill.unlocked)
|
||||||
.map(([id, skill]) => ({
|
.map(([id, _]) => getSkillDisplayInfo(player, id))
|
||||||
id,
|
.filter(Boolean)
|
||||||
name: SKILL_CONFIG[id]?.name || id,
|
|
||||||
type: SKILL_CONFIG[id]?.type || 'passive',
|
|
||||||
icon: SKILL_CONFIG[id]?.icon || '',
|
|
||||||
level: skill.level,
|
|
||||||
exp: skill.exp,
|
|
||||||
maxExp: SKILL_CONFIG[id]?.expPerLevel(skill.level + 1) || 100
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
||||||
|
|
||||||
if (currentSkillFilter.value === 'all') {
|
if (currentSkillFilter.value === 'all') {
|
||||||
@@ -201,33 +368,246 @@ const filteredSkills = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const finalStats = computed(() => {
|
const finalStats = computed(() => {
|
||||||
// 计算最终属性(基础值 + 装备加成)
|
// 计算最终属性(基础值 + 装备加成 + 全局加成)
|
||||||
let attack = 0
|
let attack = player.baseStats?.strength || 0
|
||||||
let defense = 0
|
let defense = 0
|
||||||
let critRate = 5
|
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
|
||||||
|
|
||||||
// 武器攻击力
|
// 装备属性加成
|
||||||
|
if (player.equipmentStats) {
|
||||||
|
attack += player.equipmentStats.attack || 0
|
||||||
|
defense += player.equipmentStats.defense || 0
|
||||||
|
critRate += player.equipmentStats.critRate || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有equipmentStats,直接从装备计算
|
||||||
|
if (!player.equipmentStats) {
|
||||||
|
// 武器攻击力(使用finalDamage如果存在)
|
||||||
if (player.equipment.weapon) {
|
if (player.equipment.weapon) {
|
||||||
attack += player.equipment.weapon.baseDamage || 0
|
const weapon = player.equipment.weapon
|
||||||
|
attack += weapon.finalDamage || weapon.baseDamage || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防具防御力
|
// 防具防御力(使用finalDefense如果存在)
|
||||||
if (player.equipment.armor) {
|
if (player.equipment.armor) {
|
||||||
defense += player.equipment.armor.baseDefense || 0
|
const armor = player.equipment.armor
|
||||||
|
defense += armor.finalDefense || armor.baseDefense || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算AP和EP
|
// 盾牌格挡
|
||||||
const ap = player.baseStats.dexterity + player.baseStats.intuition * 0.2
|
if (player.equipment.shield) {
|
||||||
const ep = player.baseStats.agility + player.baseStats.intuition * 0.2
|
const shield = player.equipment.shield
|
||||||
|
defense += shield.finalDefense || shield.baseDefense || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算AP和EP(包含装备加成)
|
||||||
|
const ap = (player.baseStats?.dexterity || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||||
|
const ep = (player.baseStats?.agility || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attack: Math.floor(attack),
|
attack: Math.floor(attack),
|
||||||
defense: Math.floor(defense),
|
defense: Math.floor(defense),
|
||||||
critRate: critRate,
|
critRate: Math.min(80, Math.floor(critRate)),
|
||||||
ap: Math.floor(ap),
|
ap: Math.floor(ap),
|
||||||
ep: Math.floor(ep)
|
ep: Math.floor(ep)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 书籍相关
|
||||||
|
const ownedBooks = computed(() => {
|
||||||
|
return (player.inventory || []).filter(item => item.type === 'book')
|
||||||
|
})
|
||||||
|
|
||||||
|
const readingTask = computed(() => {
|
||||||
|
const task = (game.activeTasks || []).find(t => t.type === 'reading')
|
||||||
|
if (!task) return null
|
||||||
|
|
||||||
|
const bookConfig = ITEM_CONFIG[task.data.itemId]
|
||||||
|
return {
|
||||||
|
bookName: bookConfig?.name || '未知书籍',
|
||||||
|
progress: task.progress,
|
||||||
|
totalTime: bookConfig?.readingTime || 60
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const readingTimeText = computed(() => {
|
||||||
|
if (!readingTask.value) return ''
|
||||||
|
const remaining = Math.max(0, readingTask.value.totalTime - readingTask.value.progress)
|
||||||
|
const minutes = Math.floor(remaining / 60)
|
||||||
|
const seconds = Math.floor(remaining % 60)
|
||||||
|
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 技能详情相关函数
|
||||||
|
function toggleSkillDetail(skillId) {
|
||||||
|
const index = expandedSkills.value.indexOf(skillId)
|
||||||
|
if (index > -1) {
|
||||||
|
expandedSkills.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
expandedSkills.value.push(skillId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveMilestones(skill) {
|
||||||
|
const config = SKILL_CONFIG[skill.id]
|
||||||
|
if (!config || !config.milestones) return []
|
||||||
|
|
||||||
|
return Object.entries(config.milestones)
|
||||||
|
.filter(([level]) => parseInt(level) <= skill.level)
|
||||||
|
.map(([level, milestone]) => ({
|
||||||
|
level: parseInt(level),
|
||||||
|
desc: milestone.desc
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.level - b.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpcomingMilestones(skill) {
|
||||||
|
const config = SKILL_CONFIG[skill.id]
|
||||||
|
if (!config || !config.milestones) return []
|
||||||
|
|
||||||
|
return Object.entries(config.milestones)
|
||||||
|
.filter(([level]) => parseInt(level) > skill.level)
|
||||||
|
.map(([level, milestone]) => ({
|
||||||
|
level: parseInt(level),
|
||||||
|
desc: milestone.desc
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.level - b.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startReading(book) {
|
||||||
|
const result = startTask(game, player, 'reading', {
|
||||||
|
itemId: book.id,
|
||||||
|
duration: book.readingTime || 60
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始阅读《${book.name}》`, 'info')
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练相关
|
||||||
|
const trainableSkills = computed(() => {
|
||||||
|
return Object.entries(player.skills || {})
|
||||||
|
.filter(([_, skill]) => skill.unlocked && SKILL_CONFIG[_]?.type === 'combat')
|
||||||
|
.map(([id, _]) => {
|
||||||
|
const info = getSkillDisplayInfo(player, id)
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: info?.name || id,
|
||||||
|
icon: info?.icon || '⚔️',
|
||||||
|
level: info?.level || 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
const trainingTask = computed(() => {
|
||||||
|
const task = (game.activeTasks || []).find(t => t.type === 'training')
|
||||||
|
if (!task) return null
|
||||||
|
|
||||||
|
const info = getSkillDisplayInfo(player, task.data.skillId)
|
||||||
|
return {
|
||||||
|
skillName: info?.name || '未知技能',
|
||||||
|
progress: task.progress,
|
||||||
|
totalTime: task.data.duration || 300,
|
||||||
|
taskId: task.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function startTraining(skill) {
|
||||||
|
const result = startTask(game, player, 'training', {
|
||||||
|
skillId: skill.id,
|
||||||
|
duration: 60 // 1分钟
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始训练 ${skill.name}`, 'info')
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTraining() {
|
||||||
|
if (trainingTask.value) {
|
||||||
|
endTask(game, player, trainingTask.value.taskId, false)
|
||||||
|
game.addLog('停止了训练', 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活动任务相关
|
||||||
|
const activeTasksList = computed(() => {
|
||||||
|
const tasks = (game.activeTasks || []).map(task => {
|
||||||
|
let name = '未知任务'
|
||||||
|
let total = 100
|
||||||
|
|
||||||
|
if (task.type === 'reading') {
|
||||||
|
const book = ITEM_CONFIG[task.data.itemId]
|
||||||
|
name = `阅读《${book?.name || '未知书籍'}》`
|
||||||
|
total = book?.readingTime || 60
|
||||||
|
} else if (task.type === 'training') {
|
||||||
|
const skill = SKILL_CONFIG[task.data.skillId]
|
||||||
|
name = `训练 ${skill?.name || '技能'}`
|
||||||
|
total = task.data.duration || 300
|
||||||
|
} else if (task.type === 'resting') {
|
||||||
|
name = '休息'
|
||||||
|
total = task.data.duration || 60
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = Math.min(task.progress, total)
|
||||||
|
const percentage = Math.floor((progress / total) * 100)
|
||||||
|
|
||||||
|
// 计算剩余时间
|
||||||
|
const remaining = Math.max(0, total - progress)
|
||||||
|
const minutes = Math.floor(remaining / 60)
|
||||||
|
const seconds = Math.floor(remaining % 60)
|
||||||
|
let timeText = ''
|
||||||
|
if (minutes > 0) {
|
||||||
|
timeText = `剩余 ${minutes}分${seconds}秒`
|
||||||
|
} else {
|
||||||
|
timeText = `剩余 ${seconds}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
name,
|
||||||
|
progress,
|
||||||
|
total,
|
||||||
|
percentage,
|
||||||
|
timeText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tasks.sort((a, b) => b.progress - a.progress)
|
||||||
|
})
|
||||||
|
|
||||||
|
function cancelActiveTask(task) {
|
||||||
|
const { endTask } = require('@/utils/taskSystem')
|
||||||
|
endTask(game, player, task.id, false)
|
||||||
|
game.addLog(`取消了 ${task.name}`, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 义体相关辅助函数
|
||||||
|
function formatProstheticStats(stats) {
|
||||||
|
if (!stats) return ''
|
||||||
|
const parts = []
|
||||||
|
if (stats.strength) parts.push(`力+${stats.strength}`)
|
||||||
|
if (stats.agility) parts.push(`敏+${stats.agility}`)
|
||||||
|
if (stats.dexterity) parts.push(`灵+${stats.dexterity}`)
|
||||||
|
if (stats.intuition) parts.push(`智+${stats.intuition}`)
|
||||||
|
if (stats.vitality) parts.push(`体+${stats.vitality}`)
|
||||||
|
if (stats.attack) parts.push(`攻+${stats.attack}`)
|
||||||
|
if (stats.defense) parts.push(`防+${stats.defense}`)
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProstheticSkillName(skillId) {
|
||||||
|
const { SKILL_CONFIG } = require('@/config/skills')
|
||||||
|
const skill = SKILL_CONFIG[skillId]
|
||||||
|
return skill ? skill.name : skillId
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -331,12 +711,19 @@ const finalStats = computed(() => {
|
|||||||
|
|
||||||
.skill-item {
|
.skill-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 12rpx;
|
|
||||||
padding: 12rpx;
|
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
@@ -364,6 +751,81 @@ const finalStats = computed(() => {
|
|||||||
&__bar {
|
&__bar {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 20rpx;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__expand {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__detail {
|
||||||
|
padding: 12rpx;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-detail {
|
||||||
|
&__section {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
background-color: rgba(78, 205, 196, 0.1);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
|
||||||
|
&.upcoming {
|
||||||
|
background-color: rgba(255, 107, 107, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus-level {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus-desc {
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
padding: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty-text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,8 +873,26 @@ const finalStats = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__quality {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
@@ -420,4 +900,180 @@ const finalStats = computed(() => {
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 书籍相关样式
|
||||||
|
.reading-task {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练相关样式
|
||||||
|
.training-task {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainable-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainable-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__level {
|
||||||
|
display: block;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活动任务样式
|
||||||
|
.active-tasks-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-task-item {
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 成就系统配置
|
||||||
|
*/
|
||||||
|
export const ACHIEVEMENT_CONFIG = {
|
||||||
|
// ===== 战斗成就 =====
|
||||||
|
first_blood: {
|
||||||
|
id: 'first_blood',
|
||||||
|
name: '初露锋芒',
|
||||||
|
description: '击败第一个敌人',
|
||||||
|
icon: '⚔️',
|
||||||
|
condition: { type: 'kill', count: 1 },
|
||||||
|
reward: { copper: 100, exp: 20 }
|
||||||
|
},
|
||||||
|
|
||||||
|
hunter: {
|
||||||
|
id: 'hunter',
|
||||||
|
name: '狩猎者',
|
||||||
|
description: '击败10个敌人',
|
||||||
|
icon: '🏹',
|
||||||
|
condition: { type: 'kill', count: 10 },
|
||||||
|
reward: { copper: 500, exp: 100 }
|
||||||
|
},
|
||||||
|
|
||||||
|
slayer: {
|
||||||
|
id: 'slayer',
|
||||||
|
name: '屠戮者',
|
||||||
|
description: '击败100个敌人',
|
||||||
|
icon: '💀',
|
||||||
|
condition: { type: 'kill', count: 100 },
|
||||||
|
reward: { copper: 5000, exp: 1000, item: 'iron_sword' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Boss成就 =====
|
||||||
|
boss_slayer: {
|
||||||
|
id: 'boss_slayer',
|
||||||
|
name: '弑君者',
|
||||||
|
description: '击败一个Boss',
|
||||||
|
icon: '👑',
|
||||||
|
condition: { type: 'boss_kill', count: 1 },
|
||||||
|
reward: { copper: 2000, exp: 500 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 义体成就 =====
|
||||||
|
prosthetic_user: {
|
||||||
|
id: 'prosthetic_user',
|
||||||
|
name: '机械飞升',
|
||||||
|
description: '安装第一个义体',
|
||||||
|
icon: '🦾',
|
||||||
|
condition: { type: 'equip_prosthetic', count: 1 },
|
||||||
|
reward: { copper: 1000, exp: 200 }
|
||||||
|
},
|
||||||
|
|
||||||
|
full_conversion: {
|
||||||
|
id: 'full_conversion',
|
||||||
|
name: '完全机械化',
|
||||||
|
description: '同时装备4个义体',
|
||||||
|
icon: '🤖',
|
||||||
|
condition: { type: 'equip_prosthetic', count: 4 },
|
||||||
|
reward: { copper: 10000, exp: 2000 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 技能成就 =====
|
||||||
|
skill_master: {
|
||||||
|
id: 'skill_master',
|
||||||
|
name: '宗师',
|
||||||
|
description: '任意技能达到20级',
|
||||||
|
icon: '📜',
|
||||||
|
condition: { type: 'skill_level', level: 20 },
|
||||||
|
reward: { copper: 3000, exp: 500 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 探索成就 =====
|
||||||
|
explorer: {
|
||||||
|
id: 'explorer',
|
||||||
|
name: '探险家',
|
||||||
|
description: '访问所有区域',
|
||||||
|
icon: '🗺️',
|
||||||
|
condition: { type: 'visit_locations', count: 99 },
|
||||||
|
reward: { copper: 2000, exp: 300 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 财富成就 =====
|
||||||
|
wealthy: {
|
||||||
|
id: 'wealthy',
|
||||||
|
name: '小有积蓄',
|
||||||
|
description: '累计获得10000铜币',
|
||||||
|
icon: '💰',
|
||||||
|
condition: { type: 'total_earned', amount: 10000 },
|
||||||
|
reward: { item: 'lucky_ring' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 生存成就 =====
|
||||||
|
survivor: {
|
||||||
|
id: 'survivor',
|
||||||
|
name: '幸存者',
|
||||||
|
description: '存活10天',
|
||||||
|
icon: '🌅',
|
||||||
|
condition: { type: 'survive_days', days: 10 },
|
||||||
|
reward: { copper: 1000, exp: 200 }
|
||||||
|
},
|
||||||
|
|
||||||
|
veteran: {
|
||||||
|
id: 'veteran',
|
||||||
|
name: '老兵',
|
||||||
|
description: '存活30天',
|
||||||
|
icon: '🎖️',
|
||||||
|
condition: { type: 'survive_days', days: 30 },
|
||||||
|
reward: { copper: 5000, exp: 1000, item: 'iron_armor' }
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-7
@@ -1,4 +1,4 @@
|
|||||||
// 游戏数值常量
|
// 游戏数值常量 - 与数学模型文档保持一致
|
||||||
export const GAME_CONSTANTS = {
|
export const GAME_CONSTANTS = {
|
||||||
// 时间流速:现实1秒 = 游戏时间5分钟
|
// 时间流速:现实1秒 = 游戏时间5分钟
|
||||||
TIME_SCALE: 5,
|
TIME_SCALE: 5,
|
||||||
@@ -14,13 +14,49 @@ export const GAME_CONSTANTS = {
|
|||||||
// 经验倍率
|
// 经验倍率
|
||||||
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
|
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
|
||||||
|
|
||||||
// 品质等级
|
// 品质等级 - 与数学模型文档一致
|
||||||
QUALITY_LEVELS: {
|
QUALITY_LEVELS: {
|
||||||
1: { name: '垃圾', color: '#808080', range: [0, 49], multiplier: 1.0 },
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
||||||
3: { name: '优秀', color: '#4ade80', range: [100, 129], multiplier: 1.1 },
|
3: { name: '优秀', color: '#22c55e', range: [100, 139], multiplier: 1.2 },
|
||||||
4: { name: '稀有', color: '#60a5fa', range: [130, 159], multiplier: 1.3 },
|
4: { name: '稀有', color: '#3b82f6', range: [140, 169], multiplier: 1.5 },
|
||||||
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 1.6 },
|
5: { name: '史诗', color: '#a855f7', range: [170, 199], multiplier: 2.0 },
|
||||||
6: { name: '传说', color: '#f97316', range: [200, 250], multiplier: 2.0 }
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 属性缩放系数
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0, // 1力量 = 1攻击力
|
||||||
|
AGILITY_TO_DODGE: 0.5, // 1敏捷 = 0.5%闪避
|
||||||
|
DEXTERITY_TO_CRIT: 0.3, // 1灵巧 = 0.3%暴击
|
||||||
|
INTUITION_TO_QUALITY: 0.5, // 1智力 = 0.5品质
|
||||||
|
VITALITY_TO_HP: 10, // 1体质 = 10生命
|
||||||
|
VITALITY_TO_REGEN: 0.05 // 1体质 = 0.05 HP/秒回复
|
||||||
|
},
|
||||||
|
|
||||||
|
// 装备品质系统
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01, // 品质每1点 = 1%属性
|
||||||
|
LEVEL_BONUS: 2, // 每级额外+2品质
|
||||||
|
MAX_QUALITY: 250,
|
||||||
|
MIN_QUALITY: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 词缀系统
|
||||||
|
AFFIX: {
|
||||||
|
MAX_COUNT: 3, // 每件装备最多词缀数量
|
||||||
|
COMMON_CHANCE: 0.60, // 普通词缀概率
|
||||||
|
RARE_CHANCE: 0.30, // 稀有词缀概率
|
||||||
|
EPIC_CHANCE: 0.09, // 史诗词缀概率
|
||||||
|
LEGENDARY_CHANCE: 0.01 // 传说词缀概率
|
||||||
|
},
|
||||||
|
|
||||||
|
// 怪物缩放
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12, // 每级HP增长12%
|
||||||
|
ATTACK_SCALING: 1.08, // 每级攻击增长8%
|
||||||
|
DEFENSE_SCALING: 1.06, // 每级防御增长6%
|
||||||
|
EXP_SCALING: 1.10, // 每级经验奖励增长10%
|
||||||
|
LEVEL_VARANCE: 0.2 // 等级浮动±20%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+259
-20
@@ -1,45 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* 敌人配置
|
||||||
|
* Phase 4 数值调整 - 战斗数值平衡
|
||||||
|
*/
|
||||||
|
|
||||||
// 敌人配置
|
// 敌人配置
|
||||||
export const ENEMY_CONFIG = {
|
export const ENEMY_CONFIG = {
|
||||||
|
// ===== Lv.1 敌人 =====
|
||||||
wild_dog: {
|
wild_dog: {
|
||||||
id: 'wild_dog',
|
id: 'wild_dog',
|
||||||
name: '野狗',
|
name: '野狗',
|
||||||
level: 1,
|
level: 1,
|
||||||
baseStats: {
|
baseStats: {
|
||||||
health: 30,
|
health: 35,
|
||||||
attack: 8,
|
attack: 8,
|
||||||
defense: 2,
|
defense: 2,
|
||||||
speed: 1.0
|
strength: 8, // 力量,影响攻击
|
||||||
|
agility: 6, // 敏捷,影响闪避(降低EP,提高玩家命中率)
|
||||||
|
dexterity: 7, // 灵巧,影响命中
|
||||||
|
intuition: 3, // 智力,影响AP/EP
|
||||||
|
vitality: 8, // 体质
|
||||||
|
speed: 1.2
|
||||||
},
|
},
|
||||||
derivedStats: {
|
derivedStats: {
|
||||||
ap: 10, // 攻击点数
|
ap: 8,
|
||||||
ep: 8 // 闪避点数
|
ep: 6 // 降低EP值,使玩家更容易命中
|
||||||
},
|
},
|
||||||
expReward: 15,
|
expReward: 20,
|
||||||
skillExpReward: 10,
|
skillExpReward: 12,
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'dog_skin', chance: 0.8, count: { min: 1, max: 1 } }
|
{ itemId: 'dog_skin', chance: 0.7, count: { min: 1, max: 1 } },
|
||||||
|
{ itemId: 'healing_herb', chance: 0.15, count: { min: 1, max: 2 } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
test_boss: {
|
rat: {
|
||||||
id: 'test_boss',
|
id: 'rat',
|
||||||
name: '测试Boss',
|
name: '老鼠',
|
||||||
level: 5,
|
level: 1,
|
||||||
baseStats: {
|
baseStats: {
|
||||||
health: 200,
|
health: 20,
|
||||||
attack: 25,
|
attack: 5,
|
||||||
defense: 10,
|
defense: 1,
|
||||||
speed: 0.8
|
strength: 5,
|
||||||
|
agility: 10, // 高敏捷,难命中
|
||||||
|
dexterity: 6,
|
||||||
|
intuition: 2,
|
||||||
|
vitality: 5,
|
||||||
|
speed: 1.5
|
||||||
},
|
},
|
||||||
derivedStats: {
|
derivedStats: {
|
||||||
ap: 25,
|
ap: 6,
|
||||||
ep: 15
|
ep: 11 // 高闪避,符合老鼠特性
|
||||||
},
|
},
|
||||||
expReward: 150,
|
expReward: 10,
|
||||||
skillExpReward: 50,
|
skillExpReward: 6,
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'basement_key', chance: 1.0, count: { min: 1, max: 1 } }
|
{ itemId: 'healing_herb', chance: 0.2, count: { min: 1, max: 1 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Lv.3 敌人 =====
|
||||||
|
wolf: {
|
||||||
|
id: 'wolf',
|
||||||
|
name: '灰狼',
|
||||||
|
level: 3,
|
||||||
|
baseStats: {
|
||||||
|
health: 50,
|
||||||
|
attack: 15,
|
||||||
|
defense: 5,
|
||||||
|
strength: 14,
|
||||||
|
agility: 10,
|
||||||
|
dexterity: 12,
|
||||||
|
intuition: 6,
|
||||||
|
vitality: 12,
|
||||||
|
speed: 1.3
|
||||||
|
},
|
||||||
|
derivedStats: {
|
||||||
|
ap: 14,
|
||||||
|
ep: 11
|
||||||
|
},
|
||||||
|
expReward: 40,
|
||||||
|
skillExpReward: 20,
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'wolf_fang', chance: 0.6, count: { min: 1, max: 2 } },
|
||||||
|
{ itemId: 'dog_skin', chance: 0.3, count: { min: 1, max: 1 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
bandit: {
|
||||||
|
id: 'bandit',
|
||||||
|
name: '流浪者',
|
||||||
|
level: 3,
|
||||||
|
baseStats: {
|
||||||
|
health: 60,
|
||||||
|
attack: 18,
|
||||||
|
defense: 8,
|
||||||
|
strength: 16,
|
||||||
|
agility: 9,
|
||||||
|
dexterity: 14,
|
||||||
|
intuition: 8,
|
||||||
|
vitality: 15,
|
||||||
|
speed: 1.0
|
||||||
|
},
|
||||||
|
derivedStats: {
|
||||||
|
ap: 16,
|
||||||
|
ep: 10
|
||||||
|
},
|
||||||
|
expReward: 50,
|
||||||
|
skillExpReward: 25,
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'copper_coin', chance: 0.8, count: { min: 3, max: 8 } },
|
||||||
|
{ itemId: 'bread', chance: 0.3, count: { min: 1, max: 2 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Lv.5 Boss =====
|
||||||
|
test_boss: {
|
||||||
|
id: 'test_boss',
|
||||||
|
name: '流浪狗王',
|
||||||
|
level: 5,
|
||||||
|
baseStats: {
|
||||||
|
health: 250,
|
||||||
|
attack: 30,
|
||||||
|
defense: 15,
|
||||||
|
strength: 25,
|
||||||
|
agility: 14,
|
||||||
|
dexterity: 20,
|
||||||
|
intuition: 12,
|
||||||
|
vitality: 25,
|
||||||
|
speed: 1.1
|
||||||
|
},
|
||||||
|
derivedStats: {
|
||||||
|
ap: 30,
|
||||||
|
ep: 16
|
||||||
|
},
|
||||||
|
expReward: 200,
|
||||||
|
skillExpReward: 80,
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'basement_key', chance: 1.0, count: { min: 1, max: 1 } },
|
||||||
|
{ itemId: 'dog_skin', chance: 0.5, count: { min: 2, max: 4 } },
|
||||||
|
{ itemId: 'steel_arm_prosthetic', chance: 0.3, count: { min: 1, max: 1 }, fixedQuality: true } // 义体掉落
|
||||||
|
],
|
||||||
|
isBoss: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Lv.7 敌人 =====
|
||||||
|
cave_bat: {
|
||||||
|
id: 'cave_bat',
|
||||||
|
name: '洞穴蝙蝠',
|
||||||
|
level: 7,
|
||||||
|
baseStats: {
|
||||||
|
health: 40,
|
||||||
|
attack: 25,
|
||||||
|
defense: 3,
|
||||||
|
strength: 20,
|
||||||
|
agility: 16, // 高闪避
|
||||||
|
dexterity: 18,
|
||||||
|
intuition: 6,
|
||||||
|
vitality: 8,
|
||||||
|
speed: 1.8
|
||||||
|
},
|
||||||
|
derivedStats: {
|
||||||
|
ap: 22,
|
||||||
|
ep: 18 // 高闪避,但不是不可能命中
|
||||||
|
},
|
||||||
|
expReward: 70,
|
||||||
|
skillExpReward: 35,
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'bat_wing', chance: 0.5, count: { min: 1, max: 2 } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Lv.10 Boss =====
|
||||||
|
cave_boss: {
|
||||||
|
id: 'cave_boss',
|
||||||
|
name: '洞穴领主',
|
||||||
|
level: 10,
|
||||||
|
baseStats: {
|
||||||
|
health: 500,
|
||||||
|
attack: 50,
|
||||||
|
defense: 25,
|
||||||
|
strength: 45,
|
||||||
|
agility: 18,
|
||||||
|
dexterity: 35,
|
||||||
|
intuition: 20,
|
||||||
|
vitality: 50,
|
||||||
|
speed: 0.9
|
||||||
|
},
|
||||||
|
derivedStats: {
|
||||||
|
ap: 50,
|
||||||
|
ep: 22
|
||||||
|
},
|
||||||
|
expReward: 500,
|
||||||
|
skillExpReward: 200,
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'rare_gem', chance: 0.8, count: { min: 1, max: 1 } },
|
||||||
|
{ itemId: 'iron_sword', chance: 0.3, count: { min: 1, max: 1 } },
|
||||||
|
{ itemId: 'optical_eye_prosthetic', chance: 0.4, count: { min: 1, max: 1 }, fixedQuality: true },
|
||||||
|
{ itemId: 'dermal_armor_prosthetic', chance: 0.3, count: { min: 1, max: 1 }, fixedQuality: true }
|
||||||
],
|
],
|
||||||
isBoss: true
|
isBoss: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据区域获取可用的敌人列表
|
||||||
|
* @param {String} locationId - 位置ID
|
||||||
|
* @returns {Array} 敌人ID列表
|
||||||
|
*/
|
||||||
|
export function getEnemiesForLocation(locationId) {
|
||||||
|
const locationEnemies = {
|
||||||
|
'wild1': ['wild_dog', 'rat'],
|
||||||
|
'wild2': ['wolf', 'bandit'],
|
||||||
|
'wild3': ['cave_bat', 'wolf'],
|
||||||
|
'boss_lair': ['test_boss'],
|
||||||
|
'cave_depth': ['cave_bat', 'cave_boss']
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationEnemies[locationId] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取位置的敌人完整配置
|
||||||
|
* @param {String} locationId - 位置ID
|
||||||
|
* @returns {Array} 敌人配置对象列表
|
||||||
|
*/
|
||||||
|
export function getEnemyConfigsForLocation(locationId) {
|
||||||
|
const enemyIds = getEnemiesForLocation(locationId)
|
||||||
|
return enemyIds.map(id => ENEMY_CONFIG[id]).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据等级获取推荐的敌人
|
||||||
|
* @param {Number} playerLevel - 玩家等级
|
||||||
|
* @returns {Array} 敌人ID列表
|
||||||
|
*/
|
||||||
|
export function getRecommendedEnemies(playerLevel) {
|
||||||
|
const enemies = []
|
||||||
|
|
||||||
|
for (const [id, enemy] of Object.entries(ENEMY_CONFIG)) {
|
||||||
|
const levelDiff = Math.abs(enemy.level - playerLevel)
|
||||||
|
// 推荐等级差在3以内的敌人
|
||||||
|
if (levelDiff <= 3 && !enemy.isBoss) {
|
||||||
|
enemies.push({ id, levelDiff, enemy })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按等级差排序
|
||||||
|
enemies.sort((a, b) => a.levelDiff - b.levelDiff)
|
||||||
|
return enemies.map(e => e.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机获取位置的一个敌人
|
||||||
|
* @param {String} locationId - 位置ID
|
||||||
|
* @returns {Object|null} 敌人配置
|
||||||
|
*/
|
||||||
|
export function getRandomEnemyForLocation(locationId) {
|
||||||
|
const enemyIds = getEnemiesForLocation(locationId)
|
||||||
|
if (enemyIds.length === 0) return null
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * enemyIds.length)
|
||||||
|
const enemyId = enemyIds[randomIndex]
|
||||||
|
return ENEMY_CONFIG[enemyId] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有敌人列表
|
||||||
|
* @returns {Array} 所有敌人ID
|
||||||
|
*/
|
||||||
|
export function getAllEnemyIds() {
|
||||||
|
return Object.keys(ENEMY_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Boss列表
|
||||||
|
* @returns {Array} Boss敌人ID
|
||||||
|
*/
|
||||||
|
export function getBossIds() {
|
||||||
|
return Object.entries(ENEMY_CONFIG)
|
||||||
|
.filter(([_, config]) => config.isBoss)
|
||||||
|
.map(([id, _]) => id)
|
||||||
|
}
|
||||||
|
|||||||
+50
-1
@@ -3,6 +3,8 @@
|
|||||||
* Phase 6 核心系统实现
|
* Phase 6 核心系统实现
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LOCATION_CONFIG } from './locations.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件配置
|
* 事件配置
|
||||||
* 每个事件包含:
|
* 每个事件包含:
|
||||||
@@ -236,9 +238,56 @@ export const EVENT_CONFIG = {
|
|||||||
title: '新区域',
|
title: '新区域',
|
||||||
textTemplate: '解锁了新区域:{areaName}\n\n{description}',
|
textTemplate: '解锁了新区域:{areaName}\n\n{description}',
|
||||||
choices: [
|
choices: [
|
||||||
{ text: '前往探索', next: null, action: 'teleport', actionData: { locationId: '{areaId}' } },
|
|
||||||
{ text: '稍后再说', next: null }
|
{ text: '稍后再说', next: null }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// 探索事件
|
||||||
|
explore_nothing: {
|
||||||
|
id: 'explore_nothing',
|
||||||
|
type: 'info',
|
||||||
|
title: '探索结果',
|
||||||
|
text: '你仔细搜索了这片区域,但没有发现任何有用的东西。',
|
||||||
|
choices: [
|
||||||
|
{ text: '继续', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explore_find_herb: {
|
||||||
|
id: 'explore_find_herb',
|
||||||
|
type: 'reward',
|
||||||
|
title: '探索发现',
|
||||||
|
text: '你在草丛中发现了一些草药!',
|
||||||
|
choices: [
|
||||||
|
{ text: '收下', next: null, action: 'give_item', actionData: { itemId: 'healing_herb', count: { min: 1, max: 3 } } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explore_find_coin: {
|
||||||
|
id: 'explore_find_coin',
|
||||||
|
type: 'reward',
|
||||||
|
title: '探索发现',
|
||||||
|
textTemplate: '你在角落里发现了一些铜币!\n\n获得了 {amount} 铜币',
|
||||||
|
choices: [
|
||||||
|
{ text: '太好了', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explore_find_trash: {
|
||||||
|
id: 'explore_find_trash',
|
||||||
|
type: 'info',
|
||||||
|
title: '探索发现',
|
||||||
|
text: '你只找到了一些垃圾。',
|
||||||
|
choices: [
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explore_encounter: {
|
||||||
|
id: 'explore_encounter',
|
||||||
|
type: 'combat',
|
||||||
|
title: '遭遇',
|
||||||
|
textTemplate: '探索时突然遇到了{enemyName}!',
|
||||||
|
choices: [
|
||||||
|
{ text: '迎战', next: null },
|
||||||
|
{ text: '逃跑', next: null, action: 'flee' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* 游戏数值模型配置
|
||||||
|
*
|
||||||
|
* 本文件定义了游戏中所有数值计算的核心公式
|
||||||
|
* 包括:角色属性、装备属性、技能效果、怪物属性等
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 核心常量 ====================
|
||||||
|
|
||||||
|
export const GAME_FORMULAS = {
|
||||||
|
// 经验曲线系数
|
||||||
|
EXP_BASE: 100, // 基础经验值
|
||||||
|
EXP_GROWTH: 1.15, // 每级经验增长倍数
|
||||||
|
|
||||||
|
// 属性缩放系数
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0, // 1力量 = 1攻击力
|
||||||
|
AGILITY_TO_DODGE: 0.5, // 1敏捷 = 0.5%闪避
|
||||||
|
DEXTERITY_TO_CRIT: 0.3, // 1灵巧 = 0.3%暴击
|
||||||
|
INTUITION_TO_QUALITY: 0.5, // 1智力 = 0.5品质
|
||||||
|
VITALITY_TO_HP: 10, // 1体质 = 10生命
|
||||||
|
VITALITY_TO_REGEN: 0.05 // 1体质 = 0.05 HP/秒回复
|
||||||
|
},
|
||||||
|
|
||||||
|
// 装备品质系统
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01, // 品质每1点 = 1%属性
|
||||||
|
LEVEL_BONUS: 2, // 每级额外+2品质
|
||||||
|
MAX_QUALITY: 250,
|
||||||
|
MIN_QUALITY: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 词缀系统
|
||||||
|
AFFIX: {
|
||||||
|
MAX_COUNT: 3, // 每件装备最多词缀数量
|
||||||
|
COMMON_CHANCE: 0.60, // 普通词缀概率
|
||||||
|
RARE_CHANCE: 0.30, // 稀有词缀概率
|
||||||
|
EPIC_CHANCE: 0.09, // 史诗词缀概率
|
||||||
|
LEGENDARY_CHANCE: 0.01 // 传说词缀概率
|
||||||
|
},
|
||||||
|
|
||||||
|
// 怪物缩放
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12, // 每级HP增长12%
|
||||||
|
ATTACK_SCALING: 1.08, // 每级攻击增长8%
|
||||||
|
DEFENSE_SCALING: 1.06, // 每级防御增长6%
|
||||||
|
EXP_SCALING: 1.10, // 每级经验奖励增长10%
|
||||||
|
LEVEL_VARANCE: 0.2 // 等级浮动±20%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 角色属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算角色基础属性
|
||||||
|
* @param {Object} baseStats - 基础属性 {strength, agility, dexterity, intuition, vitality}
|
||||||
|
* @param {Number} level - 角色等级
|
||||||
|
* @returns {Object} 计算后的属性
|
||||||
|
*/
|
||||||
|
export function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_FORMULAS.STAT_SCALING
|
||||||
|
|
||||||
|
// 基础生命值 = 100 + 体质 * 10 + 等级 * 5
|
||||||
|
const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5
|
||||||
|
|
||||||
|
// 基础耐力 = 80 + 体质 * 5 + 等级 * 3
|
||||||
|
const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3
|
||||||
|
|
||||||
|
// 基础精神 = 100 + 智力 * 3
|
||||||
|
const baseSanity = 100 + (baseStats.intuition || 10) * 3
|
||||||
|
|
||||||
|
// 基础攻击力 = 力量 * 1 + 等级 * 2
|
||||||
|
const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2
|
||||||
|
|
||||||
|
// 基础防御 = 敏捷 * 0.3 + 体质 * 0.2 + 等级
|
||||||
|
const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level
|
||||||
|
|
||||||
|
// 基础暴击率 = 灵巧 * 0.3% (基础值5%)
|
||||||
|
const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
|
||||||
|
// 基础闪避率 = 敏捷 * 0.5%
|
||||||
|
const baseDodgeRate = (baseStats.agility || 8) * s.AGILITY_TO_DODGE
|
||||||
|
|
||||||
|
return {
|
||||||
|
hp: Math.floor(baseHP),
|
||||||
|
stamina: Math.floor(baseStamina),
|
||||||
|
sanity: Math.floor(baseSanity),
|
||||||
|
attack: Math.floor(baseAttack),
|
||||||
|
defense: Math.floor(baseDefense),
|
||||||
|
critRate: Math.floor(baseCritRate * 10) / 10,
|
||||||
|
dodgeRate: Math.floor(baseDodgeRate * 10) / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算装备后的最终属性
|
||||||
|
* @param {Object} baseStats - 基础属性
|
||||||
|
* @param {Object} equipmentStats - 装备提供的属性
|
||||||
|
* @returns {Object} 最终属性
|
||||||
|
*/
|
||||||
|
export function calculateFinalStats(baseStats, equipmentStats = {}) {
|
||||||
|
const final = { ...baseStats }
|
||||||
|
|
||||||
|
// 装备属性加成(百分比和固定值混合)
|
||||||
|
if (equipmentStats.attackBonus) {
|
||||||
|
final.attack += Math.floor(baseStats.attack * (equipmentStats.attackBonus / 100))
|
||||||
|
final.attack += equipmentStats.attack || 0
|
||||||
|
}
|
||||||
|
if (equipmentStats.defenseBonus) {
|
||||||
|
final.defense += Math.floor(baseStats.defense * (equipmentStats.defenseBonus / 100))
|
||||||
|
final.defense += equipmentStats.defense || 0
|
||||||
|
}
|
||||||
|
if (equipmentStats.critRate) {
|
||||||
|
final.critRate += equipmentStats.critRate
|
||||||
|
}
|
||||||
|
if (equipmentStats.dodgeRate) {
|
||||||
|
final.dodgeRate += equipmentStats.dodgeRate
|
||||||
|
}
|
||||||
|
if (equipmentStats.maxHP) {
|
||||||
|
final.hp += equipmentStats.maxHP
|
||||||
|
}
|
||||||
|
if (equipmentStats.maxStamina) {
|
||||||
|
final.stamina += equipmentStats.maxStamina
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制范围
|
||||||
|
final.critRate = Math.min(95, Math.max(0, final.critRate))
|
||||||
|
final.dodgeRate = Math.min(80, Math.max(0, final.dodgeRate))
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 装备属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算装备的最终属性
|
||||||
|
* @param {Object} baseItem - 基础物品配置
|
||||||
|
* @param {Number} quality - 品质值 (0-250)
|
||||||
|
* @param {Number} itemLevel - 物品等级
|
||||||
|
* @param {Array} affixes - 词缀列表
|
||||||
|
* @returns {Object} 计算后的装备属性
|
||||||
|
*/
|
||||||
|
export function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) {
|
||||||
|
const q = GAME_FORMULAS.QUALITY
|
||||||
|
const stats = { ...baseItem }
|
||||||
|
|
||||||
|
// 1. 品质倍率计算
|
||||||
|
// 品质倍率 = 1 + (品质 - 100) * 0.01
|
||||||
|
// 品质100 = 1.0倍,品质150 = 1.5倍,品质50 = 0.5倍
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * q.BASE_MULTIPLIER
|
||||||
|
|
||||||
|
// 2. 等级倍率
|
||||||
|
// 每级增加2%属性
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
|
||||||
|
// 3. 综合倍率
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
|
// 4. 计算基础属性
|
||||||
|
if (baseItem.baseDamage) {
|
||||||
|
stats.finalDamage = Math.max(1, Math.floor(baseItem.baseDamage * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseDefense) {
|
||||||
|
stats.finalDefense = Math.max(1, Math.floor(baseItem.baseDefense * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.blockRate) {
|
||||||
|
stats.finalBlockRate = Math.min(75, baseItem.blockRate + quality * 0.02)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 应用词缀
|
||||||
|
const affixStats = calculateAffixStats(affixes, itemLevel)
|
||||||
|
Object.assign(stats, affixStats)
|
||||||
|
|
||||||
|
// 6. 计算价值
|
||||||
|
stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier * (1 + affixes.length * 0.2))
|
||||||
|
|
||||||
|
// 7. 品质信息
|
||||||
|
stats.quality = quality
|
||||||
|
stats.itemLevel = itemLevel
|
||||||
|
stats.qualityLevel = getQualityLevel(quality)
|
||||||
|
stats.affixes = affixes
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算词缀属性
|
||||||
|
* @param {Array} affixes - 词缀列表
|
||||||
|
* @param {Number} itemLevel - 物品等级
|
||||||
|
* @returns {Object} 词缀提供的属性
|
||||||
|
*/
|
||||||
|
export function calculateAffixStats(affixes, itemLevel = 1) {
|
||||||
|
const stats = {}
|
||||||
|
|
||||||
|
for (const affix of affixes) {
|
||||||
|
// 词缀属性随物品等级缩放
|
||||||
|
const scale = 1 + (itemLevel - 1) * 0.1
|
||||||
|
|
||||||
|
switch (affix.type) {
|
||||||
|
case 'attack':
|
||||||
|
stats.attackBonus = (stats.attackBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'defense':
|
||||||
|
stats.defenseBonus = (stats.defenseBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'critRate':
|
||||||
|
stats.critRate = (stats.critRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'dodge':
|
||||||
|
stats.dodgeRate = (stats.dodgeRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'hp':
|
||||||
|
stats.maxHP = (stats.maxHP || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'stamina':
|
||||||
|
stats.maxStamina = (stats.maxStamina || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'fireDamage':
|
||||||
|
stats.fireDamage = (stats.fireDamage || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'speed':
|
||||||
|
stats.attackSpeed = (stats.attackSpeed || 1) + affix.value * 0.01 * scale
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品质等级
|
||||||
|
* 与 constants.js 中的 QUALITY_LEVELS 保持一致
|
||||||
|
*/
|
||||||
|
export function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return { level: 1, name: '垃圾', color: '#6b7280', multiplier: 0.5 }
|
||||||
|
if (quality < 100) return { level: 2, name: '普通', color: '#ffffff', multiplier: 1.0 }
|
||||||
|
if (quality < 140) return { level: 3, name: '优秀', color: '#22c55e', multiplier: 1.2 }
|
||||||
|
if (quality < 170) return { level: 4, name: '稀有', color: '#3b82f6', multiplier: 1.5 }
|
||||||
|
if (quality < 200) return { level: 5, name: '史诗', color: '#a855f7', multiplier: 2.0 }
|
||||||
|
return { level: 6, name: '传说', color: '#f59e0b', multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 词缀生成 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 词缀池定义
|
||||||
|
*/
|
||||||
|
export const AFFIX_POOL = {
|
||||||
|
// 武器词缀
|
||||||
|
weapon: [
|
||||||
|
{ id: 'sharp', name: '锋利', type: 'attack', value: 5, rarity: 'common' },
|
||||||
|
{ id: 'keen', name: '锐利', type: 'critRate', value: 3, rarity: 'common' },
|
||||||
|
{ id: 'swift', name: '轻盈', type: 'speed', value: 10, rarity: 'common' },
|
||||||
|
{ id: 'vicious', name: '残暴', type: 'attack', value: 12, rarity: 'rare' },
|
||||||
|
{ id: 'deadly', name: '致命', type: 'critRate', value: 8, rarity: 'rare' },
|
||||||
|
{ id: 'flaming', name: '烈焰', type: 'fireDamage', value: 10, rarity: 'epic' },
|
||||||
|
{ id: 'godslayer', name: '弑神', type: 'attack', value: 25, rarity: 'legendary' }
|
||||||
|
],
|
||||||
|
// 防具词缀
|
||||||
|
armor: [
|
||||||
|
{ id: 'sturdy', name: '坚固', type: 'defense', value: 5, rarity: 'common' },
|
||||||
|
{ id: 'agile', name: '灵活', type: 'dodge', value: 3, rarity: 'common' },
|
||||||
|
{ id: 'healthy', name: '健康', type: 'hp', value: 20, rarity: 'common' },
|
||||||
|
{ id: 'fortified', name: '强化', type: 'defense', value: 12, rarity: 'rare' },
|
||||||
|
{ id: 'enduring', name: '耐久', type: 'stamina', value: 15, rarity: 'rare' },
|
||||||
|
{ id: 'ironclad', name: '铁壁', type: 'defense', value: 25, rarity: 'epic' },
|
||||||
|
{ id: 'immortal', name: '不朽', type: 'hp', value: 100, rarity: 'legendary' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成词缀
|
||||||
|
* @param {String} itemType - 物品类型 (weapon/armor)
|
||||||
|
* @param {Number} quality - 品质值
|
||||||
|
* @returns {Array} 词缀数组
|
||||||
|
*/
|
||||||
|
export function generateAffixes(itemType, quality) {
|
||||||
|
const pool = AFFIX_POOL[itemType] || []
|
||||||
|
if (pool.length === 0) return []
|
||||||
|
|
||||||
|
// 根据品质决定最大词缀数量
|
||||||
|
const maxAffixes = quality < 90 ? 0
|
||||||
|
: quality < 130 ? 1
|
||||||
|
: quality < 160 ? 2
|
||||||
|
: GAME_FORMULAS.AFFIX.MAX_COUNT
|
||||||
|
|
||||||
|
if (maxAffixes === 0) return []
|
||||||
|
|
||||||
|
const affixes = []
|
||||||
|
const used = new Set()
|
||||||
|
|
||||||
|
// 决定每个词缀的稀有度
|
||||||
|
for (let i = 0; i < maxAffixes; i++) {
|
||||||
|
const roll = Math.random()
|
||||||
|
let rarity
|
||||||
|
|
||||||
|
if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE) rarity = 'legendary'
|
||||||
|
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE) rarity = 'epic'
|
||||||
|
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE + GAME_FORMULAS.AFFIX.RARE_CHANCE) rarity = 'rare'
|
||||||
|
else rarity = 'common'
|
||||||
|
|
||||||
|
// 从对应稀有度池中选择词缀
|
||||||
|
const available = pool.filter(a => a.rarity === rarity && !used.has(a.id))
|
||||||
|
|
||||||
|
// 如果该稀有度没有可用词缀,降级选择
|
||||||
|
const fallback = available.length > 0 ? available : pool.filter(a => !used.has(a.id))
|
||||||
|
|
||||||
|
if (fallback.length > 0) {
|
||||||
|
const affix = fallback[Math.floor(Math.random() * fallback.length)]
|
||||||
|
affixes.push(affix)
|
||||||
|
used.add(affix.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return affixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 怪物属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物属性
|
||||||
|
* @param {Object} baseMonster - 基础怪物配置
|
||||||
|
* @param {Number} level - 怪物等级
|
||||||
|
* @param {Number} difficulty - 难度系数 (0.8 - 1.5)
|
||||||
|
* @returns {Object} 计算后的怪物属性
|
||||||
|
*/
|
||||||
|
export function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) {
|
||||||
|
const m = GAME_FORMULAS.MONSTER
|
||||||
|
|
||||||
|
// 等级缩放公式:基础值 * (1 + 增长率)^(等级-1)
|
||||||
|
const levelBonus = Math.pow(m.HP_SCALING, level - 1)
|
||||||
|
const attackBonus = Math.pow(m.ATTACK_SCALING, level - 1)
|
||||||
|
const defenseBonus = Math.pow(m.DEFENSE_SCALING, level - 1)
|
||||||
|
const expBonus = Math.pow(m.EXP_SCALING, level - 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseMonster,
|
||||||
|
level,
|
||||||
|
// HP = 基础HP * 等级倍率 * 难度系数
|
||||||
|
hp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
maxHp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
// 攻击 = 基础攻击 * 等级倍率 * 难度系数
|
||||||
|
attack: Math.floor(baseMonster.attack * attackBonus * difficulty),
|
||||||
|
// 防御 = 基础防御 * 等级倍率 * 难度系数
|
||||||
|
defense: Math.floor(baseMonster.defense * defenseBonus * difficulty),
|
||||||
|
// 经验奖励 = 基础经验 * 等级倍率
|
||||||
|
expReward: Math.floor(baseMonster.expReward * expBonus),
|
||||||
|
// 暴击率随等级略微提升
|
||||||
|
critRate: Math.min(30, (baseMonster.critRate || 0) + level * 0.5),
|
||||||
|
// 速度随等级略微提升
|
||||||
|
speed: baseMonster.speed + level * 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物等级范围
|
||||||
|
* @param {Number} baseLevel - 基础等级
|
||||||
|
* @returns {Object} { min, max }
|
||||||
|
*/
|
||||||
|
export function getMonsterLevelRange(baseLevel) {
|
||||||
|
const v = GAME_FORMULAS.MONSTER.LEVEL_VARANCE
|
||||||
|
const variance = Math.max(1, Math.floor(baseLevel * v))
|
||||||
|
return {
|
||||||
|
min: Math.max(1, baseLevel - variance),
|
||||||
|
max: baseLevel + variance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 经验计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算升级所需经验
|
||||||
|
* 与 levelingSystem.js 中的公式保持一致
|
||||||
|
* @param {Number} currentLevel - 当前等级
|
||||||
|
* @returns {Number} 所需经验
|
||||||
|
*/
|
||||||
|
export function getExpForLevel(currentLevel) {
|
||||||
|
const f = GAME_FORMULAS
|
||||||
|
if (currentLevel <= 1) return 0
|
||||||
|
// 混合曲线公式: base * (1.15 ^ (level - 2)) * (level - 1) * 0.8
|
||||||
|
// 与 levelingSystem.js 的 HYBRID 曲线保持一致
|
||||||
|
return Math.floor(f.EXP_BASE * Math.pow(f.EXP_GROWTH, currentLevel - 2) * (currentLevel - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算战斗经验
|
||||||
|
* @param {Number} monsterLevel - 怪物等级
|
||||||
|
* @param {Number} playerLevel - 玩家等级
|
||||||
|
* @param {Number} baseExp - 基础经验
|
||||||
|
* @returns {Number} 实际获得经验
|
||||||
|
*/
|
||||||
|
export function calculateCombatExp(monsterLevel, playerLevel, baseExp) {
|
||||||
|
// 等级差惩罚
|
||||||
|
const levelDiff = monsterLevel - playerLevel
|
||||||
|
|
||||||
|
let multiplier = 1.0
|
||||||
|
|
||||||
|
if (levelDiff > 5) {
|
||||||
|
// 怪物高5级以上,额外奖励
|
||||||
|
multiplier = 1 + (levelDiff - 5) * 0.1
|
||||||
|
} else if (levelDiff < -5) {
|
||||||
|
// 怪物低5级以上,大幅衰减
|
||||||
|
multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1)
|
||||||
|
} else if (levelDiff < 0) {
|
||||||
|
// 怪物等级略低,小幅衰减
|
||||||
|
multiplier = 1 + levelDiff * 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(1, Math.floor(baseExp * multiplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算技能经验
|
||||||
|
* @param {Number} skillLevel - 技能当前等级
|
||||||
|
* @param {Number} actionExp - 动作基础经验
|
||||||
|
* @returns {Number} 实际获得经验
|
||||||
|
*/
|
||||||
|
export function calculateSkillExp(skillLevel, actionExp) {
|
||||||
|
// 等级越高,获得经验越难
|
||||||
|
const decay = Math.pow(0.95, skillLevel - 1)
|
||||||
|
return Math.max(1, Math.floor(actionExp * decay))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 伤害计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算普通伤害
|
||||||
|
* @param {Number} attack - 攻击力
|
||||||
|
* @param {Number} defense - 防御力
|
||||||
|
* @param {Number} level - 等级加成
|
||||||
|
* @returns {Number} 伤害值
|
||||||
|
*/
|
||||||
|
export function calculateDamage(attack, defense, level = 1) {
|
||||||
|
// 基础伤害 = 攻击力 - 防御力 * 0.5
|
||||||
|
// 最小伤害为攻击力的20%
|
||||||
|
const baseDamage = attack - defense * 0.5
|
||||||
|
const minDamage = attack * 0.2
|
||||||
|
|
||||||
|
// 加入等级加成
|
||||||
|
const levelBonus = 1 + level * 0.02
|
||||||
|
|
||||||
|
return Math.max(minDamage, baseDamage) * levelBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算暴击伤害
|
||||||
|
* @param {Number} baseDamage - 基础伤害
|
||||||
|
* @param {Number} critMultiplier - 暴击倍率
|
||||||
|
* @returns {Number} 暴击伤害
|
||||||
|
*/
|
||||||
|
export function calculateCritDamage(baseDamage, critMultiplier = 1.5) {
|
||||||
|
return Math.floor(baseDamage * critMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算是否暴击
|
||||||
|
* @param {Number} critRate - 暴击率 (0-100)
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function rollCrit(critRate) {
|
||||||
|
return Math.random() * 100 < critRate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算是否命中
|
||||||
|
* @param {Number} hitRate - 命中率 (0-100)
|
||||||
|
* @param {Number} dodgeRate - 闪避率 (0-100)
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function rollHit(hitRate = 90, dodgeRate = 0) {
|
||||||
|
const actualHitRate = Math.max(5, Math.min(95, hitRate - dodgeRate))
|
||||||
|
return Math.random() * 100 < actualHitRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 价值计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品出售价格
|
||||||
|
* @param {Number} baseValue - 基础价值
|
||||||
|
* @param {Number} quality - 品质
|
||||||
|
* @param {Number} affixCount - 词缀数量
|
||||||
|
* @returns {Number} 出售价格
|
||||||
|
*/
|
||||||
|
export function calculateSellPrice(baseValue, quality = 100, affixCount = 0) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * 0.005
|
||||||
|
const affixMultiplier = 1 + affixCount * 0.15
|
||||||
|
return Math.floor(baseValue * qualityMultiplier * affixMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品购买价格
|
||||||
|
* @param {Number} sellPrice - 出售价格
|
||||||
|
* @param {Number} merchantRate - 商人倍率 (通常 2.0 - 3.0)
|
||||||
|
* @returns {Number} 购买价格
|
||||||
|
*/
|
||||||
|
export function calculateBuyPrice(sellPrice, merchantRate = 2.0) {
|
||||||
|
return Math.floor(sellPrice * merchantRate)
|
||||||
|
}
|
||||||
+505
-12
@@ -1,21 +1,172 @@
|
|||||||
// 物品配置
|
// 物品配置
|
||||||
|
// Phase 5 内容扩展 - 更多物品
|
||||||
export const ITEM_CONFIG = {
|
export const ITEM_CONFIG = {
|
||||||
// 武器
|
// ===== 武器 =====
|
||||||
wooden_stick: {
|
wooden_stick: {
|
||||||
id: 'wooden_stick',
|
id: 'wooden_stick',
|
||||||
name: '木棍',
|
name: '木棍',
|
||||||
type: 'weapon',
|
type: 'weapon',
|
||||||
subtype: 'one_handed',
|
subtype: 'one_handed',
|
||||||
icon: '🪵',
|
icon: '🪵',
|
||||||
baseValue: 10, // 基础价值(铜币)
|
baseValue: 10,
|
||||||
baseDamage: 5,
|
baseDamage: 5,
|
||||||
attackSpeed: 1.0,
|
attackSpeed: 1.0,
|
||||||
quality: 100, // 默认品质
|
quality: 100,
|
||||||
unlockSkill: 'stick_mastery',
|
unlockSkill: 'stick_mastery',
|
||||||
description: '一根粗糙的木棍,至少比空手强。'
|
description: '一根粗糙的木棍,至少比空手强。'
|
||||||
},
|
},
|
||||||
|
|
||||||
// 消耗品
|
rusty_sword: {
|
||||||
|
id: 'rusty_sword',
|
||||||
|
name: '生锈铁剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword',
|
||||||
|
icon: '🗡️',
|
||||||
|
baseValue: 100,
|
||||||
|
baseDamage: 12,
|
||||||
|
attackSpeed: 1.1,
|
||||||
|
quality: 50,
|
||||||
|
unlockSkill: 'sword_mastery',
|
||||||
|
stats: { critRate: 2 },
|
||||||
|
description: '一把生锈的铁剑,虽然旧了但依然锋利。'
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_sword: {
|
||||||
|
id: 'iron_sword',
|
||||||
|
name: '铁剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword',
|
||||||
|
icon: '⚔️',
|
||||||
|
baseValue: 500,
|
||||||
|
baseDamage: 25,
|
||||||
|
attackSpeed: 1.2,
|
||||||
|
quality: 100,
|
||||||
|
unlockSkill: 'sword_mastery',
|
||||||
|
stats: { critRate: 5 },
|
||||||
|
description: '一把精工打造的铁剑。'
|
||||||
|
},
|
||||||
|
|
||||||
|
wooden_club: {
|
||||||
|
id: 'wooden_club',
|
||||||
|
name: '木棒',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'blunt',
|
||||||
|
icon: '🏏',
|
||||||
|
baseValue: 30,
|
||||||
|
baseDamage: 8,
|
||||||
|
attackSpeed: 0.9,
|
||||||
|
quality: 80,
|
||||||
|
unlockSkill: 'blunt_mastery',
|
||||||
|
description: '一根粗大的木棒,攻击力强但速度慢。'
|
||||||
|
},
|
||||||
|
|
||||||
|
stone_axe: {
|
||||||
|
id: 'stone_axe',
|
||||||
|
name: '石斧',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'axe',
|
||||||
|
icon: '🪓',
|
||||||
|
baseValue: 80,
|
||||||
|
baseDamage: 18,
|
||||||
|
attackSpeed: 0.8,
|
||||||
|
quality: 60,
|
||||||
|
unlockSkill: 'axe_mastery',
|
||||||
|
stats: { critRate: 3 },
|
||||||
|
description: '用石头打磨成的斧头,笨重但有效。'
|
||||||
|
},
|
||||||
|
|
||||||
|
hunter_bow: {
|
||||||
|
id: 'hunter_bow',
|
||||||
|
name: '猎弓',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'ranged',
|
||||||
|
icon: '🏹',
|
||||||
|
baseValue: 200,
|
||||||
|
baseDamage: 15,
|
||||||
|
attackSpeed: 1.3,
|
||||||
|
quality: 90,
|
||||||
|
unlockSkill: 'archery',
|
||||||
|
stats: { accuracy: 10 },
|
||||||
|
description: '猎人使用的弓,可以远程攻击。'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 防具 =====
|
||||||
|
rag_armor: {
|
||||||
|
id: 'rag_armor',
|
||||||
|
name: '破布护甲',
|
||||||
|
type: 'armor',
|
||||||
|
subtype: 'light',
|
||||||
|
icon: '👕',
|
||||||
|
baseValue: 20,
|
||||||
|
defense: 3,
|
||||||
|
quality: 50,
|
||||||
|
description: '用破布拼凑成的简易护甲。'
|
||||||
|
},
|
||||||
|
|
||||||
|
leather_armor: {
|
||||||
|
id: 'leather_armor',
|
||||||
|
name: '皮甲',
|
||||||
|
type: 'armor',
|
||||||
|
subtype: 'light',
|
||||||
|
icon: '🦺',
|
||||||
|
baseValue: 150,
|
||||||
|
defense: 8,
|
||||||
|
quality: 100,
|
||||||
|
stats: { evasion: 5 },
|
||||||
|
description: '用兽皮制成的轻甲,提供基础保护。'
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_armor: {
|
||||||
|
id: 'iron_armor',
|
||||||
|
name: '铁甲',
|
||||||
|
type: 'armor',
|
||||||
|
subtype: 'heavy',
|
||||||
|
icon: '🛡️',
|
||||||
|
baseValue: 800,
|
||||||
|
defense: 20,
|
||||||
|
quality: 100,
|
||||||
|
stats: { maxStamina: -10 }, // 重量影响耐力
|
||||||
|
description: '铁制重甲,防御力强但会影响行动。'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 盾牌 =====
|
||||||
|
wooden_shield: {
|
||||||
|
id: 'wooden_shield',
|
||||||
|
name: '木盾',
|
||||||
|
type: 'shield',
|
||||||
|
icon: '🛡️',
|
||||||
|
baseValue: 50,
|
||||||
|
defense: 5,
|
||||||
|
blockRate: 10,
|
||||||
|
quality: 80,
|
||||||
|
description: '简单的木制盾牌。'
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_shield: {
|
||||||
|
id: 'iron_shield',
|
||||||
|
name: '铁盾',
|
||||||
|
type: 'shield',
|
||||||
|
icon: '🛡️',
|
||||||
|
baseValue: 300,
|
||||||
|
defense: 12,
|
||||||
|
blockRate: 20,
|
||||||
|
quality: 100,
|
||||||
|
description: '坚固的铁制盾牌。'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 饰品 =====
|
||||||
|
lucky_ring: {
|
||||||
|
id: 'lucky_ring',
|
||||||
|
name: '幸运戒指',
|
||||||
|
type: 'accessory',
|
||||||
|
icon: '💍',
|
||||||
|
baseValue: 200,
|
||||||
|
quality: 100,
|
||||||
|
stats: { critRate: 5, fleeRate: 5 },
|
||||||
|
description: '一枚带来幸运的戒指。'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 消耗品 - 食物 =====
|
||||||
bread: {
|
bread: {
|
||||||
id: 'bread',
|
id: 'bread',
|
||||||
name: '面包',
|
name: '面包',
|
||||||
@@ -24,13 +175,63 @@ export const ITEM_CONFIG = {
|
|||||||
icon: '🍞',
|
icon: '🍞',
|
||||||
baseValue: 10,
|
baseValue: 10,
|
||||||
effect: {
|
effect: {
|
||||||
stamina: 20
|
stamina: 20,
|
||||||
|
health: 5
|
||||||
},
|
},
|
||||||
description: '普通的面包,可以恢复耐力。',
|
description: '普通的面包,可以恢复耐力和少量生命。',
|
||||||
stackable: true,
|
stackable: true,
|
||||||
maxStack: 99
|
maxStack: 99
|
||||||
},
|
},
|
||||||
|
|
||||||
|
meat: {
|
||||||
|
id: 'meat',
|
||||||
|
name: '肉干',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'food',
|
||||||
|
icon: '🥩',
|
||||||
|
baseValue: 15,
|
||||||
|
effect: {
|
||||||
|
stamina: 35,
|
||||||
|
health: 10
|
||||||
|
},
|
||||||
|
description: '风干的肉,营养丰富。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 50
|
||||||
|
},
|
||||||
|
|
||||||
|
cooked_meat: {
|
||||||
|
id: 'cooked_meat',
|
||||||
|
name: '烤肉',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'food',
|
||||||
|
icon: '🍖',
|
||||||
|
baseValue: 25,
|
||||||
|
effect: {
|
||||||
|
stamina: 50,
|
||||||
|
health: 20
|
||||||
|
},
|
||||||
|
description: '烤制的肉,美味又营养。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 50
|
||||||
|
},
|
||||||
|
|
||||||
|
fresh_water: {
|
||||||
|
id: 'fresh_water',
|
||||||
|
name: '清水',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'drink',
|
||||||
|
icon: '💧',
|
||||||
|
baseValue: 5,
|
||||||
|
effect: {
|
||||||
|
stamina: 10,
|
||||||
|
sanity: 5
|
||||||
|
},
|
||||||
|
description: '干净的清水,解渴提神。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 消耗品 - 药品 =====
|
||||||
healing_herb: {
|
healing_herb: {
|
||||||
id: 'healing_herb',
|
id: 'healing_herb',
|
||||||
name: '草药',
|
name: '草药',
|
||||||
@@ -39,30 +240,123 @@ export const ITEM_CONFIG = {
|
|||||||
icon: '🌿',
|
icon: '🌿',
|
||||||
baseValue: 15,
|
baseValue: 15,
|
||||||
effect: {
|
effect: {
|
||||||
health: 15
|
health: 20
|
||||||
},
|
},
|
||||||
description: '常见的治疗草药,可以恢复生命值。',
|
description: '常见的治疗草药,可以恢复生命值。',
|
||||||
stackable: true,
|
stackable: true,
|
||||||
maxStack: 99
|
maxStack: 99
|
||||||
},
|
},
|
||||||
|
|
||||||
// 书籍
|
bandage: {
|
||||||
|
id: 'bandage',
|
||||||
|
name: '绷带',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'medicine',
|
||||||
|
icon: '🩹',
|
||||||
|
baseValue: 20,
|
||||||
|
effect: {
|
||||||
|
health: 30,
|
||||||
|
stamina: 5
|
||||||
|
},
|
||||||
|
description: '急救用的绷带,可以止血。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 50
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion_small: {
|
||||||
|
id: 'health_potion_small',
|
||||||
|
name: '小治疗药水',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'medicine',
|
||||||
|
icon: '🧪',
|
||||||
|
baseValue: 50,
|
||||||
|
effect: {
|
||||||
|
health: 50
|
||||||
|
},
|
||||||
|
description: '小瓶治疗药水,快速恢复生命值。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 20
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion: {
|
||||||
|
id: 'health_potion',
|
||||||
|
name: '治疗药水',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'medicine',
|
||||||
|
icon: '🧪',
|
||||||
|
baseValue: 150,
|
||||||
|
effect: {
|
||||||
|
health: 100
|
||||||
|
},
|
||||||
|
description: '治疗药水,大幅恢复生命值。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 书籍 =====
|
||||||
old_book: {
|
old_book: {
|
||||||
id: 'old_book',
|
id: 'old_book',
|
||||||
name: '破旧书籍',
|
name: '破旧书籍',
|
||||||
type: 'book',
|
type: 'book',
|
||||||
icon: '📖',
|
icon: '📖',
|
||||||
baseValue: 50,
|
baseValue: 50,
|
||||||
readingTime: 60, // 秒
|
readingTime: 60,
|
||||||
expReward: {
|
expReward: {
|
||||||
reading: 10
|
reading: 10
|
||||||
},
|
},
|
||||||
completionBonus: null,
|
completionBonus: null,
|
||||||
description: '一本破旧的书籍,记录着一些基础知识。',
|
description: '一本破旧的书籍,记录着一些基础知识。',
|
||||||
consumable: false // 书籍不消耗
|
consumable: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// 素材
|
survival_guide: {
|
||||||
|
id: 'survival_guide',
|
||||||
|
name: '生存指南',
|
||||||
|
type: 'book',
|
||||||
|
icon: '📕',
|
||||||
|
baseValue: 100,
|
||||||
|
readingTime: 120,
|
||||||
|
expReward: {
|
||||||
|
reading: 25,
|
||||||
|
survival_instinct: 5
|
||||||
|
},
|
||||||
|
completionBonus: { maxStamina: 10 },
|
||||||
|
description: '荒野生存技巧指南。',
|
||||||
|
consumable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
combat_manual: {
|
||||||
|
id: 'combat_manual',
|
||||||
|
name: '战斗手册',
|
||||||
|
type: 'book',
|
||||||
|
icon: '📗',
|
||||||
|
baseValue: 150,
|
||||||
|
readingTime: 180,
|
||||||
|
expReward: {
|
||||||
|
reading: 30
|
||||||
|
},
|
||||||
|
completionBonus: { critRate: 3 },
|
||||||
|
description: '记录战斗技巧的手册。',
|
||||||
|
consumable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
herbalism_book: {
|
||||||
|
id: 'herbalism_book',
|
||||||
|
name: '草药图鉴',
|
||||||
|
type: 'book',
|
||||||
|
icon: '📙',
|
||||||
|
baseValue: 120,
|
||||||
|
readingTime: 150,
|
||||||
|
expReward: {
|
||||||
|
reading: 20,
|
||||||
|
herbalism: 10
|
||||||
|
},
|
||||||
|
completionBonus: null,
|
||||||
|
description: '识别和采集草药的图鉴。',
|
||||||
|
consumable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 素材 =====
|
||||||
dog_skin: {
|
dog_skin: {
|
||||||
id: 'dog_skin',
|
id: 'dog_skin',
|
||||||
name: '狗皮',
|
name: '狗皮',
|
||||||
@@ -74,7 +368,74 @@ export const ITEM_CONFIG = {
|
|||||||
maxStack: 99
|
maxStack: 99
|
||||||
},
|
},
|
||||||
|
|
||||||
// 关键道具
|
wolf_fang: {
|
||||||
|
id: 'wolf_fang',
|
||||||
|
name: '狼牙',
|
||||||
|
type: 'material',
|
||||||
|
icon: '🦷',
|
||||||
|
baseValue: 20,
|
||||||
|
description: '锋利的狼牙,可用于制作武器。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99
|
||||||
|
},
|
||||||
|
|
||||||
|
bat_wing: {
|
||||||
|
id: 'bat_wing',
|
||||||
|
name: '蝙蝠翼',
|
||||||
|
type: 'material',
|
||||||
|
icon: '🦇',
|
||||||
|
baseValue: 15,
|
||||||
|
description: '蝙蝠的翅膀,有特殊用途。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99
|
||||||
|
},
|
||||||
|
|
||||||
|
leather: {
|
||||||
|
id: 'leather',
|
||||||
|
name: '皮革',
|
||||||
|
type: 'material',
|
||||||
|
icon: '🟤',
|
||||||
|
baseValue: 30,
|
||||||
|
description: '加工过的兽皮,可用于制作装备。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_ore: {
|
||||||
|
id: 'iron_ore',
|
||||||
|
name: '铁矿石',
|
||||||
|
type: 'material',
|
||||||
|
icon: '⛰️',
|
||||||
|
baseValue: 50,
|
||||||
|
description: '含铁的矿石,可以提炼金属。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99
|
||||||
|
},
|
||||||
|
|
||||||
|
rare_gem: {
|
||||||
|
id: 'rare_gem',
|
||||||
|
name: '稀有宝石',
|
||||||
|
type: 'material',
|
||||||
|
icon: '💎',
|
||||||
|
baseValue: 500,
|
||||||
|
description: '闪闪发光的宝石,价值不菲。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 货币 =====
|
||||||
|
copper_coin: {
|
||||||
|
id: 'copper_coin',
|
||||||
|
name: '铜币',
|
||||||
|
type: 'currency',
|
||||||
|
icon: '🪙',
|
||||||
|
baseValue: 1,
|
||||||
|
description: '通用的货币单位。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 9999
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 关键道具 =====
|
||||||
basement_key: {
|
basement_key: {
|
||||||
id: 'basement_key',
|
id: 'basement_key',
|
||||||
name: '地下室钥匙',
|
name: '地下室钥匙',
|
||||||
@@ -83,5 +444,137 @@ export const ITEM_CONFIG = {
|
|||||||
baseValue: 0,
|
baseValue: 0,
|
||||||
description: '一把生锈的钥匙,上面刻着「B」字母。',
|
description: '一把生锈的钥匙,上面刻着「B」字母。',
|
||||||
stackable: false
|
stackable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
cave_key: {
|
||||||
|
id: 'cave_key',
|
||||||
|
name: '洞穴钥匙',
|
||||||
|
type: 'key',
|
||||||
|
icon: '🗝️',
|
||||||
|
baseValue: 0,
|
||||||
|
description: '开启深处洞穴的钥匙。',
|
||||||
|
stackable: false
|
||||||
|
},
|
||||||
|
|
||||||
|
mystic_key: {
|
||||||
|
id: 'mystic_key',
|
||||||
|
name: '神秘钥匙',
|
||||||
|
type: 'key',
|
||||||
|
icon: '🔮',
|
||||||
|
baseValue: 1000,
|
||||||
|
description: '一把散发着神秘光芒的钥匙,似乎能打开某扇重要的门。',
|
||||||
|
stackable: false,
|
||||||
|
keyItem: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 特殊物品 =====
|
||||||
|
bomb: {
|
||||||
|
id: 'bomb',
|
||||||
|
name: '炸弹',
|
||||||
|
type: 'special',
|
||||||
|
subtype: 'explosive',
|
||||||
|
icon: '💣',
|
||||||
|
baseValue: 100,
|
||||||
|
description: '可以造成范围伤害的爆炸物,在战斗中特别有效。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10,
|
||||||
|
effect: {
|
||||||
|
damage: 50,
|
||||||
|
radius: 1
|
||||||
|
},
|
||||||
|
consumable: true
|
||||||
|
},
|
||||||
|
|
||||||
|
bible: {
|
||||||
|
id: 'bible',
|
||||||
|
name: '圣经',
|
||||||
|
type: 'special',
|
||||||
|
icon: '📿',
|
||||||
|
baseValue: 0,
|
||||||
|
description: '一本神圣的书籍,可以用来祈祷。',
|
||||||
|
stackable: false,
|
||||||
|
effect: { sanity: 10 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 义体(仅Boss掉落,无品质) =====
|
||||||
|
steel_arm_prosthetic: {
|
||||||
|
id: 'steel_arm_prosthetic',
|
||||||
|
name: '合金臂义体',
|
||||||
|
type: 'prosthetic',
|
||||||
|
subtype: 'arm',
|
||||||
|
icon: '🦾',
|
||||||
|
baseValue: 2000,
|
||||||
|
quality: 150, // 固定品质
|
||||||
|
fixedQuality: true, // 标记为固定品质
|
||||||
|
stats: { strength: 8, attack: 10 },
|
||||||
|
skill: 'steel_arm_slash', // 提供的技能
|
||||||
|
skillUnlocked: true,
|
||||||
|
description: '用合金制成的义体手臂,大幅增强力量。可使用【合金斩击】技能。',
|
||||||
|
bossOnly: true // 仅Boss掉落
|
||||||
|
},
|
||||||
|
|
||||||
|
optical_eye_prosthetic: {
|
||||||
|
id: 'optical_eye_prosthetic',
|
||||||
|
name: '光学义眼',
|
||||||
|
type: 'prosthetic',
|
||||||
|
subtype: 'head',
|
||||||
|
icon: '👁️',
|
||||||
|
baseValue: 1800,
|
||||||
|
quality: 140,
|
||||||
|
fixedQuality: true,
|
||||||
|
stats: { dexterity: 8, intuition: 5 },
|
||||||
|
skill: 'target_lock', // 提供的技能
|
||||||
|
skillUnlocked: true,
|
||||||
|
description: '植入式义眼,提高命中和暴击。可使用【目标锁定】技能。',
|
||||||
|
bossOnly: true
|
||||||
|
},
|
||||||
|
|
||||||
|
spinal_boost_prosthetic: {
|
||||||
|
id: 'spinal_boost_prosthetic',
|
||||||
|
name: '脊柱加速器',
|
||||||
|
type: 'prosthetic',
|
||||||
|
subtype: 'spine',
|
||||||
|
icon: '🦴',
|
||||||
|
baseValue: 2500,
|
||||||
|
quality: 160,
|
||||||
|
fixedQuality: true,
|
||||||
|
stats: { agility: 10, speed: 15 },
|
||||||
|
skill: 'overdrive', // 提供的技能
|
||||||
|
skillUnlocked: true,
|
||||||
|
description: '植入脊柱的加速装置,极大提升速度。可使用【过载】技能。',
|
||||||
|
bossOnly: true
|
||||||
|
},
|
||||||
|
|
||||||
|
dermal_armor_prosthetic: {
|
||||||
|
id: 'dermal_armor_prosthetic',
|
||||||
|
name: '真皮装甲',
|
||||||
|
type: 'prosthetic',
|
||||||
|
subtype: 'body',
|
||||||
|
icon: '🦾',
|
||||||
|
baseValue: 2200,
|
||||||
|
quality: 155,
|
||||||
|
fixedQuality: true,
|
||||||
|
stats: { defense: 15, vitality: 8 },
|
||||||
|
skill: 'iron_skin', // 提供的技能
|
||||||
|
skillUnlocked: true,
|
||||||
|
description: '植入皮下的装甲层,提供强大防御。可使用【钢铁皮肤】技能。',
|
||||||
|
bossOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取物品商店分类
|
||||||
|
* @returns {Object} 分类列表
|
||||||
|
*/
|
||||||
|
export const ITEM_CATEGORIES = {
|
||||||
|
weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
armor: { id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
accessory: { id: 'accessory', name: '饰品', icon: '💍' },
|
||||||
|
prosthetic: { id: 'prosthetic', name: '义体', icon: '🦾' },
|
||||||
|
consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
book: { id: 'book', name: '书籍', icon: '📖' },
|
||||||
|
material: { id: 'material', name: '素材', icon: '📦' },
|
||||||
|
key: { id: 'key', name: '钥匙', icon: '🔑' },
|
||||||
|
special: { id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ export const LOCATION_CONFIG = {
|
|||||||
description: '一个临时的幸存者营地,相对安全。',
|
description: '一个临时的幸存者营地,相对安全。',
|
||||||
connections: ['market', 'blackmarket', 'wild1'],
|
connections: ['market', 'blackmarket', 'wild1'],
|
||||||
npcs: ['injured_adventurer'],
|
npcs: ['injured_adventurer'],
|
||||||
activities: ['rest', 'talk', 'trade']
|
activities: ['rest', 'talk', 'trade', 'crafting']
|
||||||
},
|
},
|
||||||
|
|
||||||
market: {
|
market: {
|
||||||
|
|||||||
@@ -40,5 +40,53 @@ export const NPC_CONFIG = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ripper_doc: {
|
||||||
|
id: 'ripper_doc',
|
||||||
|
name: '义体医生',
|
||||||
|
location: 'blackmarket',
|
||||||
|
icon: '👨⚕️',
|
||||||
|
dialogue: {
|
||||||
|
first: {
|
||||||
|
text: '嗯...你是新面孔。我看你的身体还很"原始",需要做点升级吗?',
|
||||||
|
choices: [
|
||||||
|
{ text: '什么是义体?', next: 'explain_prosthetic' },
|
||||||
|
{ text: '我想安装义体', next: 'install_prosthetic' },
|
||||||
|
{ text: '我想卸下义体', next: 'remove_prosthetic' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explain_prosthetic: {
|
||||||
|
text: '义体是机械植入物,可以大幅增强你的能力。它们通常只有从强大的敌人身上才能获得。装备义体后,你还能获得特殊的攻击技能。',
|
||||||
|
choices: [
|
||||||
|
{ text: '我想安装义体', next: 'install_prosthetic' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
install_prosthetic: {
|
||||||
|
text: '让我看看...你有以下义体可以安装:\n{prosthetic_list}\n\n选择一件装备吧。',
|
||||||
|
choices: [
|
||||||
|
{ text: '{refresh}', next: 'install_prosthetic', action: 'refresh_list' },
|
||||||
|
{ text: '返回', next: 'first' }
|
||||||
|
],
|
||||||
|
dynamicChoices: 'prosthetic_inventory'
|
||||||
|
},
|
||||||
|
remove_prosthetic: {
|
||||||
|
text: '卸下义体是个精细的手术...你确定吗?当前装备的义体:\n{equipped_prosthetic}',
|
||||||
|
choices: [
|
||||||
|
{ text: '确定卸下', next: null, action: 'remove_prosthetic' },
|
||||||
|
{ text: '返回', next: 'first' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
install_prosthetic: {
|
||||||
|
cost: 500 // 安装费用
|
||||||
|
},
|
||||||
|
remove_prosthetic: {
|
||||||
|
cost: 200 // 卸下费用
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* 制造配方配置
|
||||||
|
* 支持武器、防具、消耗品、特殊道具的制造
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITEM_CONFIG } from './items.js'
|
||||||
|
import { getItemCount } from '@/utils/itemSystem.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方类型枚举
|
||||||
|
*/
|
||||||
|
export const RECIPE_TYPES = {
|
||||||
|
WEAPON: 'weapon', // 武器
|
||||||
|
ARMOR: 'armor', // 防具
|
||||||
|
SHIELD: 'shield', // 盾牌
|
||||||
|
CONSUMABLE: 'consumable', // 消耗品
|
||||||
|
SPECIAL: 'special' // 特殊道具
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方配置
|
||||||
|
* 每个配方包含:
|
||||||
|
* - id: 配方ID
|
||||||
|
* - type: 配方类型
|
||||||
|
* - resultItem: 产出物品ID
|
||||||
|
* - resultCount: 产出数量
|
||||||
|
* - materials: 材料需求列表
|
||||||
|
* - requiredSkill: 所需技能ID
|
||||||
|
* - requiredSkillLevel: 所需技能等级
|
||||||
|
* - baseTime: 基础制造时间(秒)
|
||||||
|
* - baseSuccessRate: 基础成功率
|
||||||
|
* - unlockCondition: 解锁条件(可选)
|
||||||
|
*/
|
||||||
|
export const RECIPE_CONFIG = {
|
||||||
|
// ===== 武器配方 =====
|
||||||
|
wooden_club: {
|
||||||
|
id: 'wooden_club',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'wooden_club',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'club_mastery',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 30,
|
||||||
|
baseSuccessRate: 0.95
|
||||||
|
},
|
||||||
|
|
||||||
|
stone_axe: {
|
||||||
|
id: 'stone_axe',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'stone_axe',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 3 },
|
||||||
|
{ itemId: 'leather', count: 1 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'axe_mastery',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.85
|
||||||
|
},
|
||||||
|
|
||||||
|
hunter_bow: {
|
||||||
|
id: 'hunter_bow',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'hunter_bow',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 3 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'archery',
|
||||||
|
requiredSkillLevel: 3,
|
||||||
|
baseTime: 120,
|
||||||
|
baseSuccessRate: 0.80
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_sword: {
|
||||||
|
id: 'iron_sword',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'iron_sword',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 5 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'sword_mastery',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 180,
|
||||||
|
baseSuccessRate: 0.75,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 防具配方 =====
|
||||||
|
leather_armor: {
|
||||||
|
id: 'leather_armor',
|
||||||
|
type: RECIPE_TYPES.ARMOR,
|
||||||
|
resultItem: 'leather_armor',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'leather', count: 5 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.90
|
||||||
|
},
|
||||||
|
|
||||||
|
wooden_shield: {
|
||||||
|
id: 'wooden_shield',
|
||||||
|
type: RECIPE_TYPES.SHIELD,
|
||||||
|
resultItem: 'wooden_shield',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'leather', count: 2 },
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 2,
|
||||||
|
baseTime: 45,
|
||||||
|
baseSuccessRate: 0.92
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_shield: {
|
||||||
|
id: 'iron_shield',
|
||||||
|
type: RECIPE_TYPES.SHIELD,
|
||||||
|
resultItem: 'iron_shield',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 8 },
|
||||||
|
{ itemId: 'leather', count: 3 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 120,
|
||||||
|
baseSuccessRate: 0.75
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 消耗品配方 =====
|
||||||
|
bandage: {
|
||||||
|
id: 'bandage',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'bandage',
|
||||||
|
resultCount: 3,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 15,
|
||||||
|
baseSuccessRate: 0.98
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion_small: {
|
||||||
|
id: 'health_potion_small',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'health_potion_small',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'healing_herb', count: 3 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'herbalism',
|
||||||
|
requiredSkillLevel: 2,
|
||||||
|
baseTime: 30,
|
||||||
|
baseSuccessRate: 0.90
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion: {
|
||||||
|
id: 'health_potion',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'health_potion',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'healing_herb', count: 5 },
|
||||||
|
{ itemId: 'bandage', count: 1 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'herbalism',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.80
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 特殊道具 =====
|
||||||
|
bomb: {
|
||||||
|
id: 'bomb',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'bomb',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 3 },
|
||||||
|
{ itemId: 'bat_wing', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 90,
|
||||||
|
baseSuccessRate: 0.75
|
||||||
|
},
|
||||||
|
|
||||||
|
// 炸弹堆(一次制造多个)
|
||||||
|
bomb_batch: {
|
||||||
|
id: 'bomb_batch',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'bomb',
|
||||||
|
resultCount: 3,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 8 },
|
||||||
|
{ itemId: 'bat_wing', count: 5 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 8,
|
||||||
|
baseTime: 200,
|
||||||
|
baseSuccessRate: 0.70
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 剧情道具 =====
|
||||||
|
mystic_key: {
|
||||||
|
id: 'mystic_key',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'mystic_key',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'rare_gem', count: 1 },
|
||||||
|
{ itemId: 'iron_ore', count: 10 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 10,
|
||||||
|
baseTime: 300,
|
||||||
|
baseSuccessRate: 0.50,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'quest',
|
||||||
|
flag: 'found_mystic_recipe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有配方
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getAllRecipes() {
|
||||||
|
return Object.values(RECIPE_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类型的配方
|
||||||
|
* @param {String} type - 配方类型
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getRecipesByType(type) {
|
||||||
|
return Object.values(RECIPE_CONFIG).filter(recipe => recipe.type === type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Array} 可制作的配方列表
|
||||||
|
*/
|
||||||
|
export function getAvailableRecipes(playerStore) {
|
||||||
|
return Object.values(RECIPE_CONFIG)
|
||||||
|
.filter(recipe => {
|
||||||
|
// 检查技能要求
|
||||||
|
if (recipe.requiredSkill) {
|
||||||
|
const skill = playerStore.skills?.[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) return false
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查解锁条件
|
||||||
|
if (recipe.unlockCondition) {
|
||||||
|
if (recipe.unlockCondition.type === 'skill') {
|
||||||
|
const skill = playerStore.skills?.[recipe.unlockCondition.skillId]
|
||||||
|
if (!skill || skill.level < recipe.unlockCondition.level) return false
|
||||||
|
} else if (recipe.unlockCondition.type === 'quest') {
|
||||||
|
if (!playerStore.flags?.[recipe.unlockCondition.flag]) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map(recipe => ({
|
||||||
|
...recipe,
|
||||||
|
canCraft: true,
|
||||||
|
displayName: ITEM_CONFIG[recipe.resultItem]?.name || recipe.resultItem,
|
||||||
|
displayIcon: ITEM_CONFIG[recipe.resultItem]?.icon || '📦'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配方信息
|
||||||
|
* @param {String} recipeId - 配方ID
|
||||||
|
* @returns {Object|null} 配方信息
|
||||||
|
*/
|
||||||
|
export function getRecipe(recipeId) {
|
||||||
|
return RECIPE_CONFIG[recipeId] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以制造(材料是否足够)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Object} { canCraft: boolean, missingMaterials: Array }
|
||||||
|
*/
|
||||||
|
export function checkMaterials(playerStore, recipe) {
|
||||||
|
const missing = []
|
||||||
|
|
||||||
|
for (const material of recipe.materials) {
|
||||||
|
const itemCount = getItemCount(playerStore, material.itemId)
|
||||||
|
if (itemCount < material.count) {
|
||||||
|
const itemConfig = ITEM_CONFIG[material.itemId]
|
||||||
|
missing.push({
|
||||||
|
itemId: material.itemId,
|
||||||
|
itemName: itemConfig?.name || material.itemId,
|
||||||
|
need: material.count,
|
||||||
|
have: itemCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canCraft: missing.length === 0,
|
||||||
|
missingMaterials: missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方分类配置
|
||||||
|
*/
|
||||||
|
export const RECIPE_CATEGORIES = {
|
||||||
|
weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
armor: { id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
special: { id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
}
|
||||||
@@ -49,5 +49,173 @@ export const SKILL_CONFIG = {
|
|||||||
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
||||||
},
|
},
|
||||||
unlockCondition: { location: 'basement' }
|
unlockCondition: { location: 'basement' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 制造技能 =====
|
||||||
|
crafting: {
|
||||||
|
id: 'crafting',
|
||||||
|
name: '制造',
|
||||||
|
type: 'life',
|
||||||
|
category: 'crafting',
|
||||||
|
icon: '🔨',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 80,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁基础制造配方', effect: {} },
|
||||||
|
3: { desc: '制造时间-10%', effect: { craftingSpeed: 0.1 } },
|
||||||
|
5: { desc: '所有制造成功率+5%', effect: { craftingSuccessRate: 5 } },
|
||||||
|
10: { desc: '制造时间-25%', effect: { craftingSpeed: 0.25 } },
|
||||||
|
15: { desc: '所有制造成功率+10%', effect: { craftingSuccessRate: 10 } },
|
||||||
|
20: { desc: '制造品质+10', effect: { craftingQuality: 10 } }
|
||||||
|
},
|
||||||
|
unlockCondition: null
|
||||||
|
},
|
||||||
|
|
||||||
|
blacksmith: {
|
||||||
|
id: 'blacksmith',
|
||||||
|
name: '锻造',
|
||||||
|
type: 'life',
|
||||||
|
category: 'crafting',
|
||||||
|
icon: '⚒️',
|
||||||
|
maxLevel: 15,
|
||||||
|
expPerLevel: (level) => level * 120,
|
||||||
|
parentSkill: 'crafting',
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁武器锻造', effect: {} },
|
||||||
|
5: { desc: '武器品质+15', effect: { weaponQuality: 15 } },
|
||||||
|
10: { desc: '防具品质+15', effect: { armorQuality: 15 } },
|
||||||
|
15: { desc: '所有锻造成功率+15%', effect: { smithingSuccessRate: 15 } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
herbalism: {
|
||||||
|
id: 'herbalism',
|
||||||
|
name: '草药学',
|
||||||
|
type: 'life',
|
||||||
|
category: 'crafting',
|
||||||
|
icon: '🌿',
|
||||||
|
maxLevel: 15,
|
||||||
|
expPerLevel: (level) => level * 60,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁药水制作', effect: {} },
|
||||||
|
3: { desc: '药水效果+20%', effect: { potionEffect: 1.2 } },
|
||||||
|
5: { desc: '解锁高级药水', effect: {} },
|
||||||
|
10: { desc: '药水效果+50%', effect: { potionEffect: 1.5 } },
|
||||||
|
15: { desc: '所有制药成功率+20%', effect: { herbingSuccessRate: 20 } }
|
||||||
|
},
|
||||||
|
unlockCondition: null
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 义体相关技能 =====
|
||||||
|
prosthetic_adaptation: {
|
||||||
|
id: 'prosthetic_adaptation',
|
||||||
|
name: '义体适应性',
|
||||||
|
type: 'passive',
|
||||||
|
category: 'prosthetic',
|
||||||
|
icon: '🦾',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 50,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁义体装备功能', effect: {} },
|
||||||
|
3: { desc: '义体技能伤害+10%', effect: { prostheticSkillDamage: 10 } },
|
||||||
|
5: { desc: '义体耐力消耗-20%', effect: { prostheticStaminaCost: -0.2 } },
|
||||||
|
10: { desc: '义体技能伤害+25%', effect: { prostheticSkillDamage: 25 } },
|
||||||
|
15: { desc: '义体冷却时间-30%', effect: { prostheticCooldown: -0.3 } },
|
||||||
|
20: { desc: '义体所有效果+50%', effect: { prostheticAllEffect: 50 } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'item',
|
||||||
|
item: 'any_prosthetic'
|
||||||
|
},
|
||||||
|
desc: '使用义体时提升,增强义体效果'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 义体攻击技能 =====
|
||||||
|
steel_arm_slash: {
|
||||||
|
id: 'steel_arm_slash',
|
||||||
|
name: '合金斩击',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'prosthetic_skill',
|
||||||
|
icon: '⚔️',
|
||||||
|
maxLevel: 1, // 义体技能不可升级
|
||||||
|
expPerLevel: () => 0,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'equipment',
|
||||||
|
item: 'steel_arm_prosthetic'
|
||||||
|
},
|
||||||
|
desc: '合金臂义体专属技能。造成200%攻击力的伤害。',
|
||||||
|
staminaCost: 15,
|
||||||
|
cooldown: 0,
|
||||||
|
damageMultiplier: 2.0
|
||||||
|
},
|
||||||
|
|
||||||
|
target_lock: {
|
||||||
|
id: 'target_lock',
|
||||||
|
name: '目标锁定',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'prosthetic_skill',
|
||||||
|
icon: '🎯',
|
||||||
|
maxLevel: 1,
|
||||||
|
expPerLevel: () => 0,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'equipment',
|
||||||
|
item: 'optical_eye_prosthetic'
|
||||||
|
},
|
||||||
|
desc: '光学义眼专属技能。下一次攻击必定暴击。',
|
||||||
|
staminaCost: 8,
|
||||||
|
cooldown: 3,
|
||||||
|
buff: 'guaranteedCrit'
|
||||||
|
},
|
||||||
|
|
||||||
|
overdrive: {
|
||||||
|
id: 'overdrive',
|
||||||
|
name: '过载',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'prosthetic_skill',
|
||||||
|
icon: '⚡',
|
||||||
|
maxLevel: 1,
|
||||||
|
expPerLevel: () => 0,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'equipment',
|
||||||
|
item: 'spinal_boost_prosthetic'
|
||||||
|
},
|
||||||
|
desc: '脊柱加速器专属技能。3秒内攻击速度+100%。',
|
||||||
|
staminaCost: 20,
|
||||||
|
cooldown: 5,
|
||||||
|
buff: 'attackSpeedBoost'
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_skin: {
|
||||||
|
id: 'iron_skin',
|
||||||
|
name: '钢铁皮肤',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'prosthetic_skill',
|
||||||
|
icon: '🛡️',
|
||||||
|
maxLevel: 1,
|
||||||
|
expPerLevel: () => 0,
|
||||||
|
parentSkill: null,
|
||||||
|
milestones: {},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'equipment',
|
||||||
|
item: 'dermal_armor_prosthetic'
|
||||||
|
},
|
||||||
|
desc: '真皮装甲专属技能。3秒内防御力+50%。',
|
||||||
|
staminaCost: 15,
|
||||||
|
cooldown: 4,
|
||||||
|
buff: 'defenseBoost'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,427 @@
|
|||||||
|
# 游戏数值模型文档
|
||||||
|
|
||||||
|
本文档详细描述了游戏中所有数值计算的核心公式和设计理念。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [设计原则](#设计原则)
|
||||||
|
2. [角色属性系统](#角色属性系统)
|
||||||
|
3. [装备属性系统](#装备属性系统)
|
||||||
|
4. [词缀系统](#词缀系统)
|
||||||
|
5. [怪物属性系统](#怪物属性系统)
|
||||||
|
6. [经验与升级系统](#经验与升级系统)
|
||||||
|
7. [战斗伤害计算](#战斗伤害计算)
|
||||||
|
8. [经济系统](#经济系统)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
### 1. 线性与指数平衡
|
||||||
|
|
||||||
|
- **低等级(1-20)**:使用线性增长,确保新手体验平滑
|
||||||
|
- **中等级(21-50)**:引入指数增长,增加挑战
|
||||||
|
- **高等级(51+)**:指数加速,突出后期成就
|
||||||
|
|
||||||
|
### 2. 边际收益递减
|
||||||
|
|
||||||
|
- 属性越高,继续提升的成本越大
|
||||||
|
- 防止某一项属性无限堆叠
|
||||||
|
|
||||||
|
### 3. 有意义的多样性
|
||||||
|
|
||||||
|
- 不同属性有不同用途,避免单一最优解
|
||||||
|
- 词缀系统提供随机性和惊喜
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 角色属性系统
|
||||||
|
|
||||||
|
### 基础属性
|
||||||
|
|
||||||
|
| 属性 | 符号 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 力量 | STR | 10 | 影响攻击力 |
|
||||||
|
| 敏捷 | AGI | 8 | 影响闪避、防御 |
|
||||||
|
| 灵巧 | DEX | 8 | 影响暴击 |
|
||||||
|
| 智力 | INT | 10 | 影响品质、魔法 |
|
||||||
|
| 体质 | VIT | 10 | 影响生命、回复 |
|
||||||
|
|
||||||
|
### 派生属性计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础生命值
|
||||||
|
HP = 100 + VIT × 10 + Level × 5
|
||||||
|
|
||||||
|
// 基础耐力
|
||||||
|
Stamina = 80 + VIT × 5 + Level × 3
|
||||||
|
|
||||||
|
// 基础精神
|
||||||
|
Sanity = 100 + INT × 3
|
||||||
|
|
||||||
|
// 基础攻击力
|
||||||
|
Attack = STR × 1.0 + Level × 2
|
||||||
|
|
||||||
|
// 基础防御力
|
||||||
|
Defense = AGI × 0.3 + VIT × 0.2 + Level
|
||||||
|
|
||||||
|
// 基础暴击率 (%)
|
||||||
|
CritRate = 5 + DEX × 0.3
|
||||||
|
|
||||||
|
// 基础闪避率 (%)
|
||||||
|
DodgeRate = AGI × 0.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性效果示例
|
||||||
|
|
||||||
|
| 属性值 | 效果 |
|
||||||
|
|--------|------|
|
||||||
|
| STR 10 → 20 | 攻击力 +10 |
|
||||||
|
| AGI 10 → 20 | 闪避 +5%,防御 +3 |
|
||||||
|
| DEX 10 → 20 | 暴击 +3% |
|
||||||
|
| VIT 10 → 20 | HP +100,回复 +0.5/秒 |
|
||||||
|
| INT 10 → 20 | 装备品质期望 +5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 装备属性系统
|
||||||
|
|
||||||
|
### 装备品质
|
||||||
|
|
||||||
|
品质范围:0-250,影响装备属性的百分比加成。
|
||||||
|
|
||||||
|
| 品质范围 | 名称 | 倍率 | 颜色 |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| 0-49 | 垃圾 | 0.5x | 灰色 |
|
||||||
|
| 50-89 | 普通 | 1.0x | 白色 |
|
||||||
|
| 90-129 | 优秀 | 1.2x | 绿色 |
|
||||||
|
| 130-159 | 稀有 | 1.5x | 蓝色 |
|
||||||
|
| 160-199 | 史诗 | 2.0x | 紫色 |
|
||||||
|
| 200-250 | 传说 | 3.0x | 橙色 |
|
||||||
|
|
||||||
|
### 装备最终属性计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 品质倍率
|
||||||
|
QualityMultiplier = 1 + (Quality - 100) × 0.01
|
||||||
|
|
||||||
|
// 等级倍率(每级 +2%)
|
||||||
|
LevelMultiplier = 1 + (ItemLevel - 1) × 0.02
|
||||||
|
|
||||||
|
// 综合倍率
|
||||||
|
TotalMultiplier = QualityMultiplier × LevelMultiplier
|
||||||
|
|
||||||
|
// 最终伤害
|
||||||
|
FinalDamage = BaseDamage × TotalMultiplier
|
||||||
|
|
||||||
|
// 最终防御
|
||||||
|
FinalDefense = BaseDefense × TotalMultiplier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 装备品质示例
|
||||||
|
|
||||||
|
以攻击力20的基础武器为例:
|
||||||
|
|
||||||
|
| 品质 | 等级 | 最终攻击力 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 100 (普通) | 1 | 20 |
|
||||||
|
| 150 (优秀) | 1 | 30 |
|
||||||
|
| 200 (史诗) | 1 | 40 |
|
||||||
|
| 100 (普通) | 10 | 24 |
|
||||||
|
| 150 (优秀) | 10 | 36 |
|
||||||
|
| 200 (史诗) | 10 | 48 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 词缀系统
|
||||||
|
|
||||||
|
### 词缀稀有度
|
||||||
|
|
||||||
|
每件装备最多3个词缀,词缀数量和稀有度受品质影响。
|
||||||
|
|
||||||
|
| 品质范围 | 最大词缀数 | 词缀稀有度倾向 |
|
||||||
|
|----------|------------|----------------|
|
||||||
|
| 0-89 | 0 | 无 |
|
||||||
|
| 90-129 | 1 | 普通60%,稀有30% |
|
||||||
|
| 130-159 | 2 | 稀有为主 |
|
||||||
|
| 160-199 | 3 | 史诗为主 |
|
||||||
|
| 200-250 | 3 | 传说9% |
|
||||||
|
|
||||||
|
### 词缀属性
|
||||||
|
|
||||||
|
词缀属性随物品等级缩放:
|
||||||
|
```
|
||||||
|
词缀最终值 = 词缀基础值 × (1 + (物品等级 - 1) × 0.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 武器词缀示例
|
||||||
|
|
||||||
|
| 名称 | 类型 | 基础值 | 稀有度 |
|
||||||
|
|------|------|--------|--------|
|
||||||
|
| 锋利 | 攻击+5% | 5 | 普通 |
|
||||||
|
| 残暴 | 攻击+12% | 12 | 稀有 |
|
||||||
|
| 弑神 | 攻击+25% | 25 | 传说 |
|
||||||
|
| 锐利 | 暴击+3% | 3 | 普通 |
|
||||||
|
| 致命 | 暴击+8% | 8 | 稀有 |
|
||||||
|
| 烈焰 | 火伤+10 | 10 | 史诗 |
|
||||||
|
|
||||||
|
### 防具词缀示例
|
||||||
|
|
||||||
|
| 名称 | 类型 | 基础值 | 稀有度 |
|
||||||
|
|------|------|--------|--------|
|
||||||
|
| 坚固 | 防御+5% | 5 | 普通 |
|
||||||
|
| 强化 | 防御+12% | 12 | 稀有 |
|
||||||
|
| 铁壁 | 防御+25% | 25 | 史诗 |
|
||||||
|
| 灵活 | 闪避+3% | 3 | 普通 |
|
||||||
|
| 耐久 | 耐力+15 | 15 | 稀有 |
|
||||||
|
| 不朽 | 生命+100 | 100 | 传说 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 怪物属性系统
|
||||||
|
|
||||||
|
### 怪物等级缩放
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 生命值缩放(每级 +12%)
|
||||||
|
MonsterHP = BaseHP × 1.12^(Level - 1)
|
||||||
|
|
||||||
|
// 攻击力缩放(每级 +8%)
|
||||||
|
MonsterAttack = BaseAttack × 1.08^(Level - 1)
|
||||||
|
|
||||||
|
// 防御力缩放(每级 +6%)
|
||||||
|
MonsterDefense = BaseDefense × 1.06^(Level - 1)
|
||||||
|
|
||||||
|
// 经验奖励缩放(每级 +10%)
|
||||||
|
MonsterEXP = BaseEXP × 1.10^(Level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 怪物等级范围
|
||||||
|
|
||||||
|
怪物实际等级在基础等级 ±20% 范围内浮动:
|
||||||
|
```javascript
|
||||||
|
LevelVariance = BaseLevel × 0.2
|
||||||
|
MinLevel = Max(1, BaseLevel - LevelVariance)
|
||||||
|
MaxLevel = BaseLevel + LevelVariance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 难度系数
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础难度
|
||||||
|
Difficulty = 1.0
|
||||||
|
|
||||||
|
// 精英怪
|
||||||
|
Elite: Difficulty = 1.5
|
||||||
|
|
||||||
|
// Boss
|
||||||
|
Boss: Difficulty = 2.0
|
||||||
|
|
||||||
|
// 弱小怪
|
||||||
|
Weak: Difficulty = 0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 怪物属性示例
|
||||||
|
|
||||||
|
以基础攻击10的怪物为例:
|
||||||
|
|
||||||
|
| 等级 | HP | 攻击 | 防御 | 经验 |
|
||||||
|
|------|-----|------|------|------|
|
||||||
|
| 1 | 100 | 10 | 5 | 20 |
|
||||||
|
| 5 | 157 | 14 | 7 | 29 |
|
||||||
|
| 10 | 278 | 20 | 10 | 47 |
|
||||||
|
| 20 | 872 | 43 | 17 | 122 |
|
||||||
|
| 50 | 18403 | 184 | 74 | 5851 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 经验与升级系统
|
||||||
|
|
||||||
|
### 角色升级经验
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 所需经验 = 基础值 × 增长系数^(等级-1)
|
||||||
|
ExpNeeded = 100 × 1.15^(Level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 等级 | 所需经验 | 累计经验 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1 | 100 | 0 |
|
||||||
|
| 5 | 174 | 674 |
|
||||||
|
| 10 | 352 | 2261 |
|
||||||
|
| 20 | 1458 | 12674 |
|
||||||
|
| 50 | 84049 | 424058 |
|
||||||
|
|
||||||
|
### 战斗经验计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 等级差调整
|
||||||
|
LevelDiff = MonsterLevel - PlayerLevel
|
||||||
|
|
||||||
|
if (LevelDiff > 5) {
|
||||||
|
// 怪物高5级以上,额外奖励
|
||||||
|
ExpMultiplier = 1 + (LevelDiff - 5) × 0.1
|
||||||
|
} else if (LevelDiff < -5) {
|
||||||
|
// 怪物低5级以上,大幅衰减
|
||||||
|
ExpMultiplier = Max(0.1, 0.5 + (LevelDiff + 5) × 0.1)
|
||||||
|
} else {
|
||||||
|
// 等级相近,正常或小幅衰减
|
||||||
|
ExpMultiplier = 1 + LevelDiff × 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际经验
|
||||||
|
ActualExp = BaseExp × ExpMultiplier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 经验衰减表
|
||||||
|
|
||||||
|
| 玩家等级 vs 怪物等级 | 经验倍率 |
|
||||||
|
|---------------------|----------|
|
||||||
|
| +5级或更高 | 100%+ |
|
||||||
|
| 相同等级 | 100% |
|
||||||
|
| -3级 | 85% |
|
||||||
|
| -5级 | 50% |
|
||||||
|
| -10级 | 10% |
|
||||||
|
|
||||||
|
### 技能经验
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 技能经验衰减(等级越高越难升级)
|
||||||
|
Decay = 0.95^(SkillLevel - 1)
|
||||||
|
ActualExp = ActionExp × Decay
|
||||||
|
```
|
||||||
|
|
||||||
|
| 技能等级 | 经验衰减 | 获得100经验实际得到 |
|
||||||
|
|----------|----------|-------------------|
|
||||||
|
| 1 | 100% | 100 |
|
||||||
|
| 5 | 81% | 81 |
|
||||||
|
| 10 | 63% | 63 |
|
||||||
|
| 20 | 36% | 36 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 战斗伤害计算
|
||||||
|
|
||||||
|
### 普通伤害
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础伤害
|
||||||
|
BaseDamage = Attack - Defense × 0.5
|
||||||
|
|
||||||
|
// 最小伤害保证(攻击力的20%)
|
||||||
|
MinDamage = Attack × 0.2
|
||||||
|
|
||||||
|
// 实际伤害
|
||||||
|
ActualDamage = Max(MinDamage, BaseDamage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暴击系统
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 暴击判定
|
||||||
|
IsCrit = Random(0, 100) < CritRate
|
||||||
|
|
||||||
|
// 暴击伤害
|
||||||
|
if (IsCrit) {
|
||||||
|
CritDamage = BaseDamage × 1.5 // 基础暴击倍率
|
||||||
|
// 可通过装备/技能提升
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命中系统
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 实际命中率
|
||||||
|
ActualHitRate = Clamp(5, 95, HitRate - DodgeRate)
|
||||||
|
|
||||||
|
// 命中判定
|
||||||
|
IsHit = Random(0, 100) < ActualHitRate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 伤害示例
|
||||||
|
|
||||||
|
| 攻击 | 防御 | 基础伤害 | 最小伤害 | 实际伤害 |
|
||||||
|
|------|------|----------|----------|----------|
|
||||||
|
| 20 | 5 | 17.5 | 4 | 17-18 |
|
||||||
|
| 50 | 10 | 45 | 10 | 45 |
|
||||||
|
| 50 | 30 | 35 | 10 | 35 |
|
||||||
|
| 100 | 50 | 75 | 20 | 75 |
|
||||||
|
| 30 | 40 | 10 | 6 | 10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 经济系统
|
||||||
|
|
||||||
|
### 物品价值
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 出售价格
|
||||||
|
SellPrice = BaseValue × QualityMultiplier × (1 + AffixCount × 0.15)
|
||||||
|
|
||||||
|
// 购买价格(商人倍率)
|
||||||
|
BuyPrice = SellPrice × MerchantRate
|
||||||
|
|
||||||
|
// 商人收购价
|
||||||
|
BuybackPrice = SellPrice × 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 商人类型
|
||||||
|
|
||||||
|
| 商人 | 出售倍率 | 收购倍率 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 普通商人 | 200% | 30% |
|
||||||
|
| 黑市商人 | 250% | 50% |
|
||||||
|
| 特殊商人 | 150% | 40% |
|
||||||
|
|
||||||
|
### 价值示例
|
||||||
|
|
||||||
|
以基础价值100的物品为例:
|
||||||
|
|
||||||
|
| 品质 | 词缀 | 出售价 | 购买价(普通) | 购买价(黑市) |
|
||||||
|
|------|------|--------|-------------|-------------|
|
||||||
|
| 100 | 0 | 100 | 200 | 250 |
|
||||||
|
| 150 | 1 | 145 | 290 | 362 |
|
||||||
|
| 200 | 2 | 250 | 500 | 625 |
|
||||||
|
| 200 | 3 | 287 | 574 | 717 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 平衡性检查
|
||||||
|
|
||||||
|
### 攻击期望
|
||||||
|
|
||||||
|
玩家攻击期望 = 基础攻击 × (1 + 暴击率 × 暴击倍率)
|
||||||
|
|
||||||
|
例如:攻击100,暴击率20%,暴击倍率1.5
|
||||||
|
```
|
||||||
|
期望伤害 = 100 × (1 + 0.2 × 0.5) = 100 × 1.1 = 110
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生存期望
|
||||||
|
|
||||||
|
玩家有效生命 = HP × (1 + 闪避率 / (1 - 闪避率))
|
||||||
|
|
||||||
|
例如:HP 1000,闪避30%
|
||||||
|
```
|
||||||
|
有效生命 = 1000 × (1 + 0.3 / 0.7) = 1000 × 1.43 = 1430
|
||||||
|
```
|
||||||
|
|
||||||
|
### DPT(每回合伤害)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
PlayerDPT = PlayerAttack × PlayerHitRate
|
||||||
|
MonsterDPT = MonsterAttack × (1 - PlayerDodgeRate)
|
||||||
|
|
||||||
|
// 生存回合
|
||||||
|
SurvivalTurns = PlayerHP / MonsterDPT
|
||||||
|
|
||||||
|
// 击杀回合
|
||||||
|
KillTurns = MonsterHP / PlayerDPT
|
||||||
|
|
||||||
|
// 战斗评价
|
||||||
|
if (KillTurns < SurvivalTurns) {
|
||||||
|
// 玩家优势
|
||||||
|
} else {
|
||||||
|
// 怪物优势
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -43,6 +43,14 @@
|
|||||||
>
|
>
|
||||||
<ShopDrawer @close="game.drawerState.shop = false" />
|
<ShopDrawer @close="game.drawerState.shop = false" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<!-- 制造抽屉 -->
|
||||||
|
<Drawer
|
||||||
|
:visible="game.drawerState.crafting"
|
||||||
|
@close="game.drawerState.crafting = false"
|
||||||
|
>
|
||||||
|
<CraftingDrawer @close="game.drawerState.crafting = false" />
|
||||||
|
</Drawer>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -58,6 +66,7 @@ import LogPanel from '@/components/panels/LogPanel.vue'
|
|||||||
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
|
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
|
||||||
import EventDrawer from '@/components/drawers/EventDrawer.vue'
|
import EventDrawer from '@/components/drawers/EventDrawer.vue'
|
||||||
import ShopDrawer from '@/components/drawers/ShopDrawer.vue'
|
import ShopDrawer from '@/components/drawers/ShopDrawer.vue'
|
||||||
|
import CraftingDrawer from '@/components/drawers/CraftingDrawer.vue'
|
||||||
|
|
||||||
const game = useGameStore()
|
const game = useGameStore()
|
||||||
const player = usePlayerStore()
|
const player = usePlayerStore()
|
||||||
|
|||||||
+78
-2
@@ -9,7 +9,8 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
const drawerState = ref({
|
const drawerState = ref({
|
||||||
inventory: false,
|
inventory: false,
|
||||||
event: false,
|
event: false,
|
||||||
shop: false
|
shop: false,
|
||||||
|
crafting: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 日志
|
// 日志
|
||||||
@@ -27,6 +28,8 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
const inCombat = ref(false)
|
const inCombat = ref(false)
|
||||||
const combatState = ref(null)
|
const combatState = ref(null)
|
||||||
const autoCombat = ref(false) // 自动战斗模式
|
const autoCombat = ref(false) // 自动战斗模式
|
||||||
|
const isSearching = ref(false) // 正在寻找敌人中(自动战斗间隔期间)
|
||||||
|
const preferredStance = ref('balance') // 记住玩家偏好的战斗姿态
|
||||||
|
|
||||||
// 活动任务
|
// 活动任务
|
||||||
const activeTasks = ref([])
|
const activeTasks = ref([])
|
||||||
@@ -43,6 +46,16 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
// 当前事件
|
// 当前事件
|
||||||
const currentEvent = ref(null)
|
const currentEvent = ref(null)
|
||||||
|
|
||||||
|
// 成就系统
|
||||||
|
const achievements = ref({})
|
||||||
|
const achievementProgress = ref({
|
||||||
|
totalKills: 0,
|
||||||
|
bossKills: 0,
|
||||||
|
prostheticEquipped: 0,
|
||||||
|
visitedLocations: new Set(),
|
||||||
|
totalEarned: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 添加日志
|
// 添加日志
|
||||||
function addLog(message, type = 'info') {
|
function addLog(message, type = 'info') {
|
||||||
const time = `${String(gameTime.value.hour).padStart(2, '0')}:${String(gameTime.value.minute).padStart(2, '0')}`
|
const time = `${String(gameTime.value.hour).padStart(2, '0')}:${String(gameTime.value.minute).padStart(2, '0')}`
|
||||||
@@ -58,6 +71,54 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查成就
|
||||||
|
function checkAchievements(triggerType, data = {}) {
|
||||||
|
const { ACHIEVEMENT_CONFIG } = require('@/config/achievements.js')
|
||||||
|
let newAchievements = []
|
||||||
|
|
||||||
|
for (const [id, achievement] of Object.entries(ACHIEVEMENT_CONFIG)) {
|
||||||
|
// 已完成的跳过
|
||||||
|
if (achievements.value[id]) continue
|
||||||
|
|
||||||
|
let unlocked = false
|
||||||
|
|
||||||
|
switch (achievement.condition.type) {
|
||||||
|
case 'kill':
|
||||||
|
unlocked = achievementProgress.value.totalKills >= achievement.condition.count
|
||||||
|
break
|
||||||
|
case 'boss_kill':
|
||||||
|
unlocked = achievementProgress.value.bossKills >= achievement.condition.count
|
||||||
|
break
|
||||||
|
case 'equip_prosthetic':
|
||||||
|
unlocked = data.prostheticCount >= achievement.condition.count
|
||||||
|
break
|
||||||
|
case 'skill_level':
|
||||||
|
unlocked = data.maxSkillLevel >= achievement.condition.level
|
||||||
|
break
|
||||||
|
case 'visit_locations':
|
||||||
|
unlocked = achievementProgress.value.visitedLocations.size >= achievement.condition.count
|
||||||
|
break
|
||||||
|
case 'total_earned':
|
||||||
|
unlocked = achievementProgress.value.totalEarned >= achievement.condition.amount
|
||||||
|
break
|
||||||
|
case 'survive_days':
|
||||||
|
unlocked = gameTime.value.day >= achievement.condition.days
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unlocked) {
|
||||||
|
achievements.value[id] = {
|
||||||
|
unlocked: true,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
newAchievements.push(achievement)
|
||||||
|
addLog(`🏆 解锁成就: ${achievement.icon} ${achievement.name}`, 'reward')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newAchievements
|
||||||
|
}
|
||||||
|
|
||||||
// 重置游戏状态
|
// 重置游戏状态
|
||||||
function resetGame() {
|
function resetGame() {
|
||||||
currentTab.value = 'status'
|
currentTab.value = 'status'
|
||||||
@@ -76,6 +137,8 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
inCombat.value = false
|
inCombat.value = false
|
||||||
combatState.value = null
|
combatState.value = null
|
||||||
autoCombat.value = false
|
autoCombat.value = false
|
||||||
|
isSearching.value = false
|
||||||
|
preferredStance.value = 'balance'
|
||||||
activeTasks.value = []
|
activeTasks.value = []
|
||||||
negativeStatus.value = []
|
negativeStatus.value = []
|
||||||
marketPrices.value = {
|
marketPrices.value = {
|
||||||
@@ -83,6 +146,14 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
prices: {}
|
prices: {}
|
||||||
}
|
}
|
||||||
currentEvent.value = null
|
currentEvent.value = null
|
||||||
|
achievements.value = {}
|
||||||
|
achievementProgress.value = {
|
||||||
|
totalKills: 0,
|
||||||
|
bossKills: 0,
|
||||||
|
prostheticEquipped: 0,
|
||||||
|
visitedLocations: new Set(),
|
||||||
|
totalEarned: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -93,11 +164,16 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
inCombat,
|
inCombat,
|
||||||
combatState,
|
combatState,
|
||||||
autoCombat,
|
autoCombat,
|
||||||
|
isSearching,
|
||||||
|
preferredStance,
|
||||||
activeTasks,
|
activeTasks,
|
||||||
negativeStatus,
|
negativeStatus,
|
||||||
marketPrices,
|
marketPrices,
|
||||||
currentEvent,
|
currentEvent,
|
||||||
|
achievements,
|
||||||
|
achievementProgress,
|
||||||
addLog,
|
addLog,
|
||||||
resetGame
|
resetGame,
|
||||||
|
checkAchievements
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+21
-4
@@ -5,7 +5,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
// 基础属性
|
// 基础属性
|
||||||
const baseStats = ref({
|
const baseStats = ref({
|
||||||
strength: 10, // 力量
|
strength: 10, // 力量
|
||||||
agility: 8, // 敏捷
|
agility: 6, // 敏捷(降低以提高被命中率)
|
||||||
dexterity: 8, // 灵巧
|
dexterity: 8, // 灵巧
|
||||||
intuition: 10, // 智力
|
intuition: 10, // 智力
|
||||||
vitality: 10 // 体质(影响HP)
|
vitality: 10 // 体质(影响HP)
|
||||||
@@ -36,9 +36,13 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
weapon: null,
|
weapon: null,
|
||||||
armor: null,
|
armor: null,
|
||||||
shield: null,
|
shield: null,
|
||||||
accessory: null
|
accessory: null,
|
||||||
|
prosthetic: null // 义体装备位
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 义体适应性(使用义体时增加)
|
||||||
|
const prostheticAdaptation = ref(0)
|
||||||
|
|
||||||
// 背包
|
// 背包
|
||||||
const inventory = ref([])
|
const inventory = ref([])
|
||||||
|
|
||||||
@@ -53,6 +57,12 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
// 标记
|
// 标记
|
||||||
const flags = ref({})
|
const flags = ref({})
|
||||||
|
|
||||||
|
// 击杀统计(用于成就和解锁条件)
|
||||||
|
const killCount = ref({})
|
||||||
|
|
||||||
|
// 访问过的位置
|
||||||
|
const visitedLocations = ref(new Set())
|
||||||
|
|
||||||
// 计算属性:总货币
|
// 计算属性:总货币
|
||||||
const totalCurrency = computed(() => {
|
const totalCurrency = computed(() => {
|
||||||
return {
|
return {
|
||||||
@@ -66,7 +76,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
function resetPlayer() {
|
function resetPlayer() {
|
||||||
baseStats.value = {
|
baseStats.value = {
|
||||||
strength: 10,
|
strength: 10,
|
||||||
agility: 8,
|
agility: 6, // 降低以提高被命中率
|
||||||
dexterity: 8,
|
dexterity: 8,
|
||||||
intuition: 10,
|
intuition: 10,
|
||||||
vitality: 10
|
vitality: 10
|
||||||
@@ -89,12 +99,16 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
weapon: null,
|
weapon: null,
|
||||||
armor: null,
|
armor: null,
|
||||||
shield: null,
|
shield: null,
|
||||||
accessory: null
|
accessory: null,
|
||||||
|
prosthetic: null
|
||||||
}
|
}
|
||||||
|
prostheticAdaptation.value = 0
|
||||||
inventory.value = []
|
inventory.value = []
|
||||||
currency.value = { copper: 0 }
|
currency.value = { copper: 0 }
|
||||||
currentLocation.value = 'camp'
|
currentLocation.value = 'camp'
|
||||||
flags.value = {}
|
flags.value = {}
|
||||||
|
killCount.value = {}
|
||||||
|
visitedLocations.value = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -103,11 +117,14 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
level,
|
level,
|
||||||
skills,
|
skills,
|
||||||
equipment,
|
equipment,
|
||||||
|
prostheticAdaptation,
|
||||||
inventory,
|
inventory,
|
||||||
currency,
|
currency,
|
||||||
totalCurrency,
|
totalCurrency,
|
||||||
currentLocation,
|
currentLocation,
|
||||||
flags,
|
flags,
|
||||||
|
killCount,
|
||||||
|
visitedLocations,
|
||||||
resetPlayer
|
resetPlayer
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,530 @@
|
|||||||
|
/**
|
||||||
|
* 数值模型测试
|
||||||
|
* 验证数学模型文档中的各种计算
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导入公式模块 - 使用require以兼容Jest
|
||||||
|
// 注意:实际使用时需要确保模块路径正确
|
||||||
|
const GAME_CONSTANTS = {
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { name: '普通', color: '#ffffff', range: [50, 89], multiplier: 1.0 },
|
||||||
|
3: { name: '优秀', color: '#22c55e', range: [90, 129], multiplier: 1.2 },
|
||||||
|
4: { name: '稀有', color: '#3b82f6', range: [130, 159], multiplier: 1.5 },
|
||||||
|
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 2.0 },
|
||||||
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01
|
||||||
|
},
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0,
|
||||||
|
AGILITY_TO_DODGE: 0.5,
|
||||||
|
DEXTERITY_TO_CRIT: 0.3,
|
||||||
|
VITALITY_TO_HP: 10
|
||||||
|
},
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12,
|
||||||
|
ATTACK_SCALING: 1.08,
|
||||||
|
DEFENSE_SCALING: 1.06,
|
||||||
|
EXP_SCALING: 1.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 核心计算函数 ====================
|
||||||
|
|
||||||
|
function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_CONSTANTS.STAT_SCALING
|
||||||
|
|
||||||
|
const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5
|
||||||
|
const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3
|
||||||
|
const baseSanity = 100 + (baseStats.intuition || 10) * 3
|
||||||
|
const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2
|
||||||
|
const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level
|
||||||
|
const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
const baseDodgeRate = (baseStats.agility || 8) * s.AGILITY_TO_DODGE
|
||||||
|
|
||||||
|
return {
|
||||||
|
hp: Math.floor(baseHP),
|
||||||
|
stamina: Math.floor(baseStamina),
|
||||||
|
sanity: Math.floor(baseSanity),
|
||||||
|
attack: Math.floor(baseAttack),
|
||||||
|
defense: Math.floor(baseDefense),
|
||||||
|
critRate: Math.floor(baseCritRate * 10) / 10,
|
||||||
|
dodgeRate: Math.floor(baseDodgeRate * 10) / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
|
const stats = { ...baseItem }
|
||||||
|
if (baseItem.baseDamage) {
|
||||||
|
stats.finalDamage = Math.max(1, Math.floor(baseItem.baseDamage * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseDefense) {
|
||||||
|
stats.finalDefense = Math.max(1, Math.floor(baseItem.baseDefense * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseValue) {
|
||||||
|
stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
const affixStats = calculateAffixStats(affixes, itemLevel)
|
||||||
|
Object.assign(stats, affixStats)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateAffixStats(affixes, itemLevel = 1) {
|
||||||
|
const stats = {}
|
||||||
|
for (const affix of affixes) {
|
||||||
|
const scale = 1 + (itemLevel - 1) * 0.1
|
||||||
|
switch (affix.type) {
|
||||||
|
case 'attack':
|
||||||
|
stats.attackBonus = (stats.attackBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'critRate':
|
||||||
|
stats.critRate = (stats.critRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) {
|
||||||
|
const m = GAME_CONSTANTS.MONSTER
|
||||||
|
const levelBonus = Math.pow(m.HP_SCALING, level - 1)
|
||||||
|
const attackBonus = Math.pow(m.ATTACK_SCALING, level - 1)
|
||||||
|
const defenseBonus = Math.pow(m.DEFENSE_SCALING, level - 1)
|
||||||
|
const expBonus = Math.pow(m.EXP_SCALING, level - 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseMonster,
|
||||||
|
level,
|
||||||
|
hp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
maxHp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
attack: Math.floor(baseMonster.attack * attackBonus * difficulty),
|
||||||
|
defense: Math.floor(baseMonster.defense * defenseBonus * difficulty),
|
||||||
|
expReward: Math.floor(baseMonster.expReward * expBonus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpForLevel(level) {
|
||||||
|
return Math.floor(100 * Math.pow(1.15, level - 2) * (level - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCombatExp(enemyLevel, playerLevel, baseExp) {
|
||||||
|
const levelDiff = enemyLevel - playerLevel
|
||||||
|
let multiplier = 1.0
|
||||||
|
if (levelDiff > 5) multiplier = 1 + (levelDiff - 5) * 0.1
|
||||||
|
else if (levelDiff < -5) multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1)
|
||||||
|
else multiplier = 1 + levelDiff * 0.05
|
||||||
|
return Math.max(1, Math.floor(baseExp * multiplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSkillExp(skillLevel, actionExp) {
|
||||||
|
return Math.max(1, Math.floor(actionExp * Math.pow(0.95, skillLevel - 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDamage(attack, defense) {
|
||||||
|
const baseDamage = attack - defense * 0.5
|
||||||
|
const minDamage = attack * 0.2
|
||||||
|
return Math.max(minDamage, baseDamage)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCritDamage(baseDamage, critMultiplier = 1.5) {
|
||||||
|
return Math.floor(baseDamage * critMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollCrit(critRate) {
|
||||||
|
return Math.random() * 100 < critRate
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSellPrice(baseValue, quality, affixCount) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * 0.005
|
||||||
|
const affixMultiplier = 1 + affixCount * 0.15
|
||||||
|
return Math.floor(baseValue * qualityMultiplier * affixMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBuyPrice(sellPrice, merchantRate = 2.0) {
|
||||||
|
return Math.floor(sellPrice * merchantRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return { name: '垃圾', color: '#6b7280', multiplier: 0.5 }
|
||||||
|
if (quality < 90) return { name: '普通', color: '#ffffff', multiplier: 1.0 }
|
||||||
|
if (quality < 130) return { name: '优秀', color: '#22c55e', multiplier: 1.2 }
|
||||||
|
if (quality < 160) return { name: '稀有', color: '#3b82f6', multiplier: 1.5 }
|
||||||
|
if (quality < 200) return { name: '史诗', color: '#a855f7', multiplier: 2.0 }
|
||||||
|
return { name: '传说', color: '#f59e0b', multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAffixes(itemType, quality) {
|
||||||
|
// 简化版本用于测试
|
||||||
|
if (quality < 90) return []
|
||||||
|
if (quality < 130) return [{ type: 'attack', value: 5 }]
|
||||||
|
if (quality < 160) return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }]
|
||||||
|
return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }, { type: 'attack', value: 12 }]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('数学模型验证', () => {
|
||||||
|
describe('角色属性计算', () => {
|
||||||
|
it('应该正确计算基础属性', () => {
|
||||||
|
const baseStats = {
|
||||||
|
strength: 10,
|
||||||
|
agility: 8,
|
||||||
|
dexterity: 8,
|
||||||
|
intuition: 10,
|
||||||
|
vitality: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = calculateCharacterStats(baseStats, 1)
|
||||||
|
|
||||||
|
// HP = 100 + 10*10 + 1*5 = 205
|
||||||
|
expect(stats.hp).toBe(205)
|
||||||
|
// Attack = 10*1 + 1*2 = 12
|
||||||
|
expect(stats.attack).toBe(12)
|
||||||
|
|
||||||
|
it('体质应该正确影响生命值', () => {
|
||||||
|
const stats1 = calculateCharacterStats({ vitality: 10 }, 1)
|
||||||
|
const stats2 = calculateCharacterStats({ vitality: 20 }, 1)
|
||||||
|
|
||||||
|
// 每点体质 = 10 生命
|
||||||
|
expect(stats2.hp - stats1.hp).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('力量应该正确影响攻击力', () => {
|
||||||
|
const stats1 = calculateCharacterStats({ strength: 10 }, 1)
|
||||||
|
const stats2 = calculateCharacterStats({ strength: 20 }, 1)
|
||||||
|
|
||||||
|
// 每点力量 = 1 攻击力
|
||||||
|
expect(stats2.attack - stats1.attack).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('装备品质计算', () => {
|
||||||
|
it('品质100应该产生1.0倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 100, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(20)
|
||||||
|
expect(result.finalValue).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('品质150应该产生1.5倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 150, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(30) // 20 * 1.5
|
||||||
|
expect(result.finalValue).toBe(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('品质50应该产生0.5倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 50, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(10) // 20 * 0.5
|
||||||
|
expect(result.finalValue).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('物品等级应该影响属性', () => {
|
||||||
|
const baseItem = { baseDamage: 20 }
|
||||||
|
const result1 = calculateEquipmentStats(baseItem, 100, 1, [])
|
||||||
|
const result2 = calculateEquipmentStats(baseItem, 100, 10, [])
|
||||||
|
|
||||||
|
// 每级 +2%
|
||||||
|
expect(result2.finalDamage).toBe(23) // 20 * 1.02^9 ≈ 23.9, rounded = 23
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('品质等级获取', () => {
|
||||||
|
it('应该正确获取品质等级', () => {
|
||||||
|
expect(getQualityLevel(0).name).toBe('垃圾')
|
||||||
|
expect(getQualityLevel(50).name).toBe('普通')
|
||||||
|
expect(getQualityLevel(90).name).toBe('优秀')
|
||||||
|
expect(getQualityLevel(130).name).toBe('稀有')
|
||||||
|
expect(getQualityLevel(160).name).toBe('史诗')
|
||||||
|
expect(getQualityLevel(200).name).toBe('传说')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该正确返回品质颜色', () => {
|
||||||
|
expect(getQualityLevel(200).color).toBe('#f59e0b')
|
||||||
|
expect(getQualityLevel(130).color).toBe('#3b82f6')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('词缀系统', () => {
|
||||||
|
it('应该计算词缀属性加成', () => {
|
||||||
|
const affixes = [
|
||||||
|
{ type: 'attack', value: 5 },
|
||||||
|
{ type: 'critRate', value: 3 }
|
||||||
|
]
|
||||||
|
const result = calculateAffixStats(affixes, 1)
|
||||||
|
|
||||||
|
expect(result.attackBonus).toBe(5)
|
||||||
|
expect(result.critRate).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('词缀属性应该随物品等级缩放', () => {
|
||||||
|
const affixes = [{ type: 'attack', value: 10 }]
|
||||||
|
const result1 = calculateAffixStats(affixes, 1)
|
||||||
|
const result2 = calculateAffixStats(affixes, 10)
|
||||||
|
|
||||||
|
// 等级10: 10 * (1 + 9 * 0.1) = 10 * 1.9 = 19
|
||||||
|
expect(result2.attackBonus).toBeGreaterThan(result1.attackBonus)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('低品质不应该生成词缀', () => {
|
||||||
|
const affixes = generateAffixes('weapon', 50)
|
||||||
|
expect(affixes.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('高品质应该生成更多词缀', () => {
|
||||||
|
const affixes = generateAffixes('weapon', 180)
|
||||||
|
expect(affixes.length).toBeGreaterThanOrEqual(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('怪物属性计算', () => {
|
||||||
|
it('应该正确缩放怪物属性', () => {
|
||||||
|
const baseMonster = {
|
||||||
|
hp: 100,
|
||||||
|
attack: 10,
|
||||||
|
defense: 5,
|
||||||
|
expReward: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
const level1 = calculateMonsterStats(baseMonster, 1, 1.0)
|
||||||
|
const level10 = calculateMonsterStats(baseMonster, 10, 1.0)
|
||||||
|
|
||||||
|
expect(level10.hp).toBeGreaterThan(level1.hp)
|
||||||
|
expect(level10.attack).toBeGreaterThan(level1.attack)
|
||||||
|
expect(level10.expReward).toBeGreaterThan(level1.expReward)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('怪物缩放应该符合文档公式', () => {
|
||||||
|
const baseMonster = { hp: 100, attack: 10, defense: 5, expReward: 20 }
|
||||||
|
|
||||||
|
// 测试5级怪物: HP = 100 * 1.12^4 = 157
|
||||||
|
const level5 = calculateMonsterStats(baseMonster, 5, 1.0)
|
||||||
|
|
||||||
|
// 1.12^4 = 1.5735...
|
||||||
|
expect(Math.round(level5.hp / 100 * 100) / 100).toBeCloseTo(1.57, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经验计算', () => {
|
||||||
|
it('升级经验应该符合指数增长', () => {
|
||||||
|
const exp2 = getExpForLevel(2)
|
||||||
|
const exp3 = getExpForLevel(3)
|
||||||
|
const exp10 = getExpForLevel(10)
|
||||||
|
|
||||||
|
expect(exp2).toBe(80)
|
||||||
|
expect(exp3).toBeCloseTo(115, 0) // 100 * 1.15
|
||||||
|
expect(exp10).toBeGreaterThan(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('等级差应该影响经验获取', () => {
|
||||||
|
const sameLevel = calculateCombatExp(10, 10, 100)
|
||||||
|
const higherLevel = calculateCombatExp(15, 10, 100)
|
||||||
|
const lowerLevel = calculateCombatExp(5, 10, 100)
|
||||||
|
|
||||||
|
expect(higherLevel).toBeGreaterThan(sameLevel)
|
||||||
|
expect(lowerLevel).toBeLessThan(sameLevel)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('过低等级怪物应该大幅衰减经验', () => {
|
||||||
|
const lowLevel = calculateCombatExp(1, 10, 100)
|
||||||
|
const veryLowLevel = calculateCombatExp(1, 20, 100)
|
||||||
|
|
||||||
|
expect(lowLevel).toBeLessThan(100)
|
||||||
|
expect(veryLowLevel).toBeLessThan(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('技能经验', () => {
|
||||||
|
it('高等级技能应该获得更少经验', () => {
|
||||||
|
const exp1 = calculateSkillExp(1, 100)
|
||||||
|
const exp10 = calculateSkillExp(10, 100)
|
||||||
|
const exp20 = calculateSkillExp(20, 100)
|
||||||
|
|
||||||
|
expect(exp10).toBeLessThan(exp1)
|
||||||
|
expect(exp20).toBeLessThan(exp10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('技能经验衰减应该符合公式', () => {
|
||||||
|
// 衰减公式: 0.95^(level-1)
|
||||||
|
const exp5 = calculateSkillExp(5, 100)
|
||||||
|
const expected = Math.floor(100 * Math.pow(0.95, 4))
|
||||||
|
|
||||||
|
expect(exp5).toBeCloseTo(expected, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('伤害计算', () => {
|
||||||
|
it('应该正确计算基础伤害', () => {
|
||||||
|
const damage = calculateDamage(50, 10, 1)
|
||||||
|
|
||||||
|
// 基础伤害 = 攻击 - 防御 * 0.5 = 50 - 5 = 45
|
||||||
|
// 最小伤害 = 攻击 * 0.2 = 10
|
||||||
|
expect(damage).toBe(45)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('防御应该减少伤害但不低于最小值', () => {
|
||||||
|
const damage1 = calculateDamage(50, 10, 1)
|
||||||
|
const damage2 = calculateDamage(50, 40, 1)
|
||||||
|
|
||||||
|
// 高防御时,伤害应该有最小值保证
|
||||||
|
expect(damage2).toBeGreaterThan(0)
|
||||||
|
expect(damage1).toBeGreaterThan(damage2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('暴击伤害应该正确计算', () => {
|
||||||
|
const critDamage = calculateCritDamage(100, 1.5)
|
||||||
|
expect(critDamage).toBe(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('暴击判定应该在阈值内正确工作', () => {
|
||||||
|
// 测试多次验证概率分布
|
||||||
|
let critCount = 0
|
||||||
|
const trials = 1000
|
||||||
|
const critRate = 50 // 50% 暴击率
|
||||||
|
|
||||||
|
for (let i = 0; i < trials; i++) {
|
||||||
|
// Mock Math.random to return 0.3 (< 50)
|
||||||
|
const originalRandom = Math.random
|
||||||
|
Math.random = () => 0.3
|
||||||
|
if (rollCrit(critRate)) critCount++
|
||||||
|
Math.random = originalRandom
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由于我们mock了random,这应该总是暴击
|
||||||
|
expect(critCount).toBe(trials)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经济系统', () => {
|
||||||
|
it('应该正确计算出售价格', () => {
|
||||||
|
const price1 = calculateSellPrice(100, 100, 0)
|
||||||
|
const price2 = calculateSellPrice(100, 150, 0)
|
||||||
|
const price3 = calculateSellPrice(100, 150, 2)
|
||||||
|
|
||||||
|
expect(price1).toBe(100) // 基础
|
||||||
|
expect(price2).toBeGreaterThan(price1) // 高品质
|
||||||
|
expect(price3).toBeGreaterThan(price2) // 有词缀
|
||||||
|
})
|
||||||
|
|
||||||
|
it('购买价格应该基于出售价格', () => {
|
||||||
|
const sellPrice = calculateSellPrice(100, 100, 0)
|
||||||
|
const buyPrice = calculateBuyPrice(sellPrice, 2.0)
|
||||||
|
|
||||||
|
expect(buyPrice).toBe(200) // 2倍商人倍率
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('游戏常量一致性', () => {
|
||||||
|
it('常量应该与数学模型一致', () => {
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[1].multiplier).toBe(0.5)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[2].multiplier).toBe(1.0)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[3].multiplier).toBe(1.2)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[4].multiplier).toBe(1.5)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[5].multiplier).toBe(2.0)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[6].multiplier).toBe(3.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('属性缩放系数应该正确', () => {
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.STRENGTH_TO_ATTACK).toBe(1.0)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.AGILITY_TO_DODGE).toBe(0.5)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.DEXTERITY_TO_CRIT).toBe(0.3)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.VITALITY_TO_HP).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('怪物缩放系数应该正确', () => {
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.HP_SCALING).toBe(1.12)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.ATTACK_SCALING).toBe(1.08)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.DEFENSE_SCALING).toBe(1.06)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.EXP_SCALING).toBe(1.10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可玩性测试
|
||||||
|
* 验证游戏在不同阶段的可玩性
|
||||||
|
*/
|
||||||
|
describe('游戏可玩性验证', () => {
|
||||||
|
describe('新手阶段 (Lv1-10)', () => {
|
||||||
|
it('新手应该能击败1级怪物', () => {
|
||||||
|
const playerStats = calculateCharacterStats({
|
||||||
|
strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10
|
||||||
|
}, 1)
|
||||||
|
|
||||||
|
const monster = calculateMonsterStats({
|
||||||
|
hp: 50, attack: 5, defense: 2, expReward: 10
|
||||||
|
}, 1, 1.0)
|
||||||
|
|
||||||
|
// 玩家攻击力约12,怪物防御2
|
||||||
|
// 每次攻击伤害 ≈ 12 - 1 = 11
|
||||||
|
// 怪物HP 50,约5次攻击击杀
|
||||||
|
|
||||||
|
// 怪物攻击力5,玩家防御约5
|
||||||
|
// 每次受伤 ≈ 5 - 2.5 = 2.5
|
||||||
|
// 玩家HP 200,能承受约80次攻击
|
||||||
|
|
||||||
|
expect(playerStats.attack).toBeGreaterThan(monster.defense)
|
||||||
|
expect(playerStats.hp).toBeGreaterThan(monster.hp / 10) // 玩家血量足够
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新手升级经验应该合理', () => {
|
||||||
|
// 1->2级需要100经验
|
||||||
|
const exp1to2 = getExpForLevel(2)
|
||||||
|
const monsterExp = 10 // 1级怪物经验
|
||||||
|
|
||||||
|
// 需要击败10只1级怪物升级
|
||||||
|
expect(exp1to2 / monsterExp).toBeLessThan(20) // 不应该超过20只
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('中期阶段 (Lv11-30)', () => {
|
||||||
|
it('中期角色应该能应对10级怪物', () => {
|
||||||
|
const playerStats = calculateCharacterStats({
|
||||||
|
strength: 20, agility: 15, dexterity: 15, intuition: 15, vitality: 15
|
||||||
|
}, 15)
|
||||||
|
|
||||||
|
const monster = calculateMonsterStats({
|
||||||
|
hp: 100, attack: 10, defense: 5, expReward: 20
|
||||||
|
}, 10, 1.0)
|
||||||
|
|
||||||
|
// 等级差距5级,怪物更强但玩家属性更高
|
||||||
|
expect(playerStats.attack).toBeGreaterThan(monster.defense * 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('战斗平衡性', () => {
|
||||||
|
it('攻击姿态应该高伤害低防御', () => {
|
||||||
|
// 这部分需要在战斗系统中测试
|
||||||
|
// 确保不同姿态有明显差异
|
||||||
|
})
|
||||||
|
|
||||||
|
it('闪避率应该有上限', () => {
|
||||||
|
// 测试闪避率计算
|
||||||
|
// 即使敏捷很高,闪避率也应该有上限
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经济平衡', () => {
|
||||||
|
it('怪物掉落应该支持购买基础装备', () => {
|
||||||
|
// 测试经济循环
|
||||||
|
// 击败怪物获得的钱应该足够购买装备
|
||||||
|
})
|
||||||
|
|
||||||
|
it('高品质装备应该明显更贵', () => {
|
||||||
|
const normalPrice = calculateSellPrice(100, 100, 0)
|
||||||
|
const epicPrice = calculateSellPrice(100, 180, 2)
|
||||||
|
|
||||||
|
expect(epicPrice).toBeGreaterThan(normalPrice * 1.5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 数值模型验证脚本
|
||||||
|
* 运行: node tests/verify-model.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 游戏常量
|
||||||
|
const GAME_CONSTANTS = {
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
||||||
|
3: { name: '优秀', color: '#22c55e', range: [100, 139], multiplier: 1.2 },
|
||||||
|
4: { name: '稀有', color: '#3b82f6', range: [140, 169], multiplier: 1.5 },
|
||||||
|
5: { name: '史诗', color: '#a855f7', range: [170, 199], multiplier: 2.0 },
|
||||||
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
QUALITY: { BASE_MULTIPLIER: 0.01 },
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0,
|
||||||
|
AGILITY_TO_DODGE: 0.5,
|
||||||
|
DEXTERITY_TO_CRIT: 0.3,
|
||||||
|
VITALITY_TO_HP: 10,
|
||||||
|
VITALITY_TO_REGEN: 0.05
|
||||||
|
},
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12,
|
||||||
|
ATTACK_SCALING: 1.08,
|
||||||
|
DEFENSE_SCALING: 1.06,
|
||||||
|
EXP_SCALING: 1.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 计算函数 ====================
|
||||||
|
|
||||||
|
function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_CONSTANTS.STAT_SCALING
|
||||||
|
return {
|
||||||
|
hp: Math.floor(100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5),
|
||||||
|
attack: Math.floor((baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2),
|
||||||
|
critRate: 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEquipmentStats(baseDamage, quality, itemLevel = 1) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
return Math.floor(baseDamage * qualityMultiplier * levelMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMonsterStats(baseHp, level) {
|
||||||
|
return Math.floor(baseHp * Math.pow(GAME_CONSTANTS.MONSTER.HP_SCALING, level - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpForLevel(level) {
|
||||||
|
return Math.floor(100 * Math.pow(1.15, level - 2) * (level - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return GAME_CONSTANTS.QUALITY_LEVELS[1]
|
||||||
|
if (quality < 100) return GAME_CONSTANTS.QUALITY_LEVELS[2]
|
||||||
|
if (quality < 140) return GAME_CONSTANTS.QUALITY_LEVELS[3]
|
||||||
|
if (quality < 170) return GAME_CONSTANTS.QUALITY_LEVELS[4]
|
||||||
|
if (quality < 200) return GAME_CONSTANTS.QUALITY_LEVELS[5]
|
||||||
|
return GAME_CONSTANTS.QUALITY_LEVELS[6]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 验证测试 ====================
|
||||||
|
|
||||||
|
console.log('=== 数值模型验证 ===\n')
|
||||||
|
|
||||||
|
// 1. 品质系统测试
|
||||||
|
console.log('1. 品质倍率验证:')
|
||||||
|
console.log(` 品质50 (垃圾): 攻击力20 -> ${calculateEquipmentStats(20, 50, 1)} (0.5x)`)
|
||||||
|
console.log(` 品质100 (普通): 攻击力20 -> ${calculateEquipmentStats(20, 100, 1)} (1.0x)`)
|
||||||
|
console.log(` 品质150 (优秀): 攻击力20 -> ${calculateEquipmentStats(20, 150, 1)} (1.5x)`)
|
||||||
|
console.log(` 品质200 (史诗): 攻击力20 -> ${calculateEquipmentStats(20, 200, 1)} (2.0x)`)
|
||||||
|
console.log(` 品质250 (传说): 攻击力20 -> ${calculateEquipmentStats(20, 250, 1)} (2.5x)`)
|
||||||
|
|
||||||
|
// 2. 物品等级测试
|
||||||
|
console.log('\n2. 物品等级影响 (品质100):')
|
||||||
|
console.log(` Lv1: 攻击力20 -> ${calculateEquipmentStats(20, 100, 1)}`)
|
||||||
|
console.log(` Lv5: 攻击力20 -> ${calculateEquipmentStats(20, 100, 5)}`)
|
||||||
|
console.log(` Lv10: 攻击力20 -> ${calculateEquipmentStats(20, 100, 10)}`)
|
||||||
|
console.log(` Lv50: 攻击力20 -> ${calculateEquipmentStats(20, 100, 50)}`)
|
||||||
|
|
||||||
|
// 3. 角色属性测试
|
||||||
|
console.log('\n3. 角色属性 (Lv1, 默认属性):')
|
||||||
|
const defaultStats = { strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10 }
|
||||||
|
const stats = calculateCharacterStats(defaultStats, 1)
|
||||||
|
console.log(` HP: ${stats.hp}`)
|
||||||
|
console.log(` 攻击: ${stats.attack}`)
|
||||||
|
console.log(` 暴击率: ${stats.critRate}%`)
|
||||||
|
console.log(` 体质+10 -> HP +${calculateCharacterStats({ vitality: 20 }, 1).hp - stats.hp}`)
|
||||||
|
|
||||||
|
// 4. 怪物缩放测试
|
||||||
|
console.log('\n4. 怪物等级缩放 (基础HP 100):')
|
||||||
|
console.log(` Lv1: HP = ${calculateMonsterStats(100, 1)}`)
|
||||||
|
console.log(` Lv5: HP = ${calculateMonsterStats(100, 5)} (1.12^4 ≈ 1.57x)`)
|
||||||
|
console.log(` Lv10: HP = ${calculateMonsterStats(100, 10)} (1.12^9 ≈ 2.8x)`)
|
||||||
|
console.log(` Lv20: HP = ${calculateMonsterStats(100, 20)} (1.12^19 ≈ 8.0x)`)
|
||||||
|
|
||||||
|
// 5. 经验曲线测试
|
||||||
|
console.log('\n5. 升级经验需求:')
|
||||||
|
console.log(` Lv1 -> Lv2: ${getExpForLevel(2)} 经验`)
|
||||||
|
console.log(` Lv5 -> Lv6: ${getExpForLevel(6)} 经验`)
|
||||||
|
console.log(` Lv10 -> Lv11: ${getExpForLevel(11)} 经验`)
|
||||||
|
console.log(` Lv20 -> Lv21: ${getExpForLevel(21)} 经验`)
|
||||||
|
console.log(` Lv50 -> Lv51: ${getExpForLevel(51)} 经验`)
|
||||||
|
|
||||||
|
// 6. 品质等级验证
|
||||||
|
console.log('\n6. 品质等级判定:')
|
||||||
|
for (let q = 0; q <= 250; q += 50) {
|
||||||
|
const level = getQualityLevel(q)
|
||||||
|
console.log(` 品质 ${q}: ${level.name.padEnd(6)} (${level.color})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 可玩性验证
|
||||||
|
console.log('\n=== 可玩性验证 ===\n')
|
||||||
|
|
||||||
|
// 新手阶段
|
||||||
|
const playerLv1 = calculateCharacterStats(defaultStats, 1)
|
||||||
|
const monsterLv1 = calculateMonsterStats(50, 1)
|
||||||
|
console.log('新手阶段 (Lv1 vs Lv1 怪物):')
|
||||||
|
console.log(` 玩家 HP: ${playerLv1.hp}, 攻击: ${playerLv1.attack}`)
|
||||||
|
console.log(` 怪物 HP: ${monsterLv1}, 攻击: ${calculateEquipmentStats(5, 100, 1)}`)
|
||||||
|
console.log(` 预期: 玩家约需 5-6 次攻击击杀怪物,能承受 20+ 次攻击`)
|
||||||
|
console.log(` 结论: ${playerLv1.attack > 5 ? '✓ 可玩' : '✗ 太难'}`)
|
||||||
|
|
||||||
|
// 中期阶段
|
||||||
|
const playerLv15 = calculateCharacterStats({
|
||||||
|
strength: 20, agility: 15, dexterity: 15, intuition: 15, vitality: 15
|
||||||
|
}, 15)
|
||||||
|
const monsterLv15 = calculateMonsterStats(100, 15)
|
||||||
|
console.log('\n中期阶段 (Lv15 vs Lv15 怪物):')
|
||||||
|
console.log(` 玩家 HP: ${playerLv15.hp}, 攻击: ${playerLv15.attack}`)
|
||||||
|
console.log(` 怪物 HP: ${monsterLv15}, 攻击: ${calculateEquipmentStats(10, 100, 15)}`)
|
||||||
|
console.log(` 预期: 玩家约需 15-20 次攻击击杀怪物`)
|
||||||
|
console.log(` 结论: ${playerLv15.attack > 20 ? '✓ 可玩' : '需要提升'}`)
|
||||||
|
|
||||||
|
// 战斗经济
|
||||||
|
console.log('\n经济系统:')
|
||||||
|
const normalItemPrice = 100 * 1.0
|
||||||
|
const epicItemPrice = 100 * 2.0
|
||||||
|
const monsterGold = 20
|
||||||
|
console.log(` 普通装备价格: ${normalItemPrice} 铜币`)
|
||||||
|
console.log(` 史诗装备价格: ${epicItemPrice} 铜币`)
|
||||||
|
console.log(` Lv1 怪物掉落: ${monsterGold} 铜币 (假设)`)
|
||||||
|
console.log(` 需要击败 ${Math.ceil(epicItemPrice / monsterGold)} 只怪物购买史诗装备`)
|
||||||
|
|
||||||
|
console.log('\n=== 验证完成 ===')
|
||||||
@@ -92,6 +92,7 @@ $accent: #4ecdc4;
|
|||||||
$danger: #ff6b6b;
|
$danger: #ff6b6b;
|
||||||
$warning: #ffe66d;
|
$warning: #ffe66d;
|
||||||
$success: #4ade80;
|
$success: #4ade80;
|
||||||
|
$info: #60a5fa;
|
||||||
|
|
||||||
/* 品质颜色 */
|
/* 品质颜色 */
|
||||||
$quality-trash: #808080;
|
$quality-trash: #808080;
|
||||||
|
|||||||
+44
-35
@@ -143,22 +143,27 @@ export function calculateEP(defender, stance = 'balance', environment = 'normal'
|
|||||||
*
|
*
|
||||||
* @param {Number} ap - 攻击点数
|
* @param {Number} ap - 攻击点数
|
||||||
* @param {Number} ep - 闪避点数
|
* @param {Number} ep - 闪避点数
|
||||||
|
* @param {Boolean} isPlayer - 是否是玩家攻击(玩家享受最低命中率加成)
|
||||||
* @returns {Number} 命中概率 (0-1)
|
* @returns {Number} 命中概率 (0-1)
|
||||||
*/
|
*/
|
||||||
export function calculateHitRate(ap, ep) {
|
export function calculateHitRate(ap, ep, isPlayer = false) {
|
||||||
const ratio = ap / (ep + 1)
|
const ratio = ap / (ep + 1)
|
||||||
|
|
||||||
// 非线性命中概率表
|
// 非线性命中概率表
|
||||||
if (ratio >= 5) return 0.98
|
let hitRate = 0.05
|
||||||
if (ratio >= 3) return 0.90
|
if (ratio >= 5) hitRate = 0.98
|
||||||
if (ratio >= 2) return 0.80
|
else if (ratio >= 3) hitRate = 0.90
|
||||||
if (ratio >= 1.5) return 0.65
|
else if (ratio >= 2) hitRate = 0.80
|
||||||
if (ratio >= 1) return 0.50
|
else if (ratio >= 1.5) hitRate = 0.65
|
||||||
if (ratio >= 0.75) return 0.38
|
else if (ratio >= 1) hitRate = 0.50
|
||||||
if (ratio >= 0.5) return 0.24
|
else if (ratio >= 0.75) hitRate = 0.38
|
||||||
if (ratio >= 0.33) return 0.15
|
else if (ratio >= 0.5) hitRate = 0.24
|
||||||
if (ratio >= 0.2) return 0.08
|
else if (ratio >= 0.33) hitRate = 0.15
|
||||||
return 0.05
|
else if (ratio >= 0.2) hitRate = 0.08
|
||||||
|
|
||||||
|
// 最低命中率:敌人攻击玩家时至少40%命中率,玩家攻击敌人时至少15%
|
||||||
|
const minHitRate = isPlayer ? 0.15 : 0.40
|
||||||
|
return Math.max(minHitRate, hitRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,13 +207,13 @@ export function calculateCritMultiplier(attacker) {
|
|||||||
* @param {Object} defenderBonuses - 防御者加成
|
* @param {Object} defenderBonuses - 防御者加成
|
||||||
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
|
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
|
||||||
*/
|
*/
|
||||||
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}) {
|
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}, isPlayerAttacker = false) {
|
||||||
// 计算AP和EP
|
// 计算AP和EP
|
||||||
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
|
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
|
||||||
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
|
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
|
||||||
|
|
||||||
// 计算命中概率
|
// 计算命中概率(玩家攻击时isPlayerAttacker=true,敌人攻击时isPlayerAttacker=false)
|
||||||
const hitRate = calculateHitRate(ap, ep)
|
const hitRate = calculateHitRate(ap, ep, isPlayerAttacker)
|
||||||
|
|
||||||
// 第一步:闪避判定
|
// 第一步:闪避判定
|
||||||
const roll = Math.random()
|
const roll = Math.random()
|
||||||
@@ -391,7 +396,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
|||||||
stance,
|
stance,
|
||||||
environment,
|
environment,
|
||||||
playerBonuses,
|
playerBonuses,
|
||||||
{}
|
{},
|
||||||
|
true // 玩家攻击
|
||||||
)
|
)
|
||||||
|
|
||||||
if (playerAttack.hit) {
|
if (playerAttack.hit) {
|
||||||
@@ -441,7 +447,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
|||||||
'balance',
|
'balance',
|
||||||
environment,
|
environment,
|
||||||
{},
|
{},
|
||||||
playerBonuses
|
playerBonuses,
|
||||||
|
false // 敌人攻击
|
||||||
)
|
)
|
||||||
|
|
||||||
if (enemyAttack.hit) {
|
if (enemyAttack.hit) {
|
||||||
@@ -516,9 +523,10 @@ export function getStaminaCost(stance) {
|
|||||||
* @param {String} enemyId - 敌人ID
|
* @param {String} enemyId - 敌人ID
|
||||||
* @param {Object} enemyConfig - 敌人配置
|
* @param {Object} enemyConfig - 敌人配置
|
||||||
* @param {String} environment - 环境类型
|
* @param {String} environment - 环境类型
|
||||||
|
* @param {String} preferredStance - 玩家偏好的战斗姿态
|
||||||
* @returns {Object} 战斗状态
|
* @returns {Object} 战斗状态
|
||||||
*/
|
*/
|
||||||
export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
export function initCombat(enemyId, enemyConfig, environment = 'normal', preferredStance = 'balance') {
|
||||||
return {
|
return {
|
||||||
enemyId,
|
enemyId,
|
||||||
enemy: {
|
enemy: {
|
||||||
@@ -530,32 +538,16 @@ export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
|||||||
defense: enemyConfig.baseStats.defense,
|
defense: enemyConfig.baseStats.defense,
|
||||||
baseStats: enemyConfig.baseStats,
|
baseStats: enemyConfig.baseStats,
|
||||||
expReward: enemyConfig.expReward,
|
expReward: enemyConfig.expReward,
|
||||||
|
skillExpReward: enemyConfig.skillExpReward || 0,
|
||||||
drops: enemyConfig.drops || []
|
drops: enemyConfig.drops || []
|
||||||
},
|
},
|
||||||
stance: 'balance',
|
stance: preferredStance, // 使用玩家偏好的战斗姿态
|
||||||
environment,
|
environment,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
ticks: 0
|
ticks: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算姿态切换后的攻击速度修正
|
|
||||||
* @param {String} stance - 姿态
|
|
||||||
* @param {Number} baseSpeed - 基础攻击速度
|
|
||||||
* @returns {Number} 修正后的攻击速度
|
|
||||||
*/
|
|
||||||
export function getAttackSpeed(stance, baseSpeed = 1.0) {
|
|
||||||
const speedModifiers = {
|
|
||||||
attack: 1.3, // 攻击姿态: 速度 +30%
|
|
||||||
defense: 0.9, // 防御姿态: 速度 -10%
|
|
||||||
balance: 1.0,
|
|
||||||
rapid: 2.0, // 快速打击: 速度 +100%
|
|
||||||
heavy: 0.5 // 重击: 速度 -50%
|
|
||||||
}
|
|
||||||
return baseSpeed * (speedModifiers[stance] || 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取姿态显示名称
|
* 获取姿态显示名称
|
||||||
* @param {String} stance - 姿态
|
* @param {String} stance - 姿态
|
||||||
@@ -572,6 +564,23 @@ export function getStanceDisplayName(stance) {
|
|||||||
return names[stance] || '平衡'
|
return names[stance] || '平衡'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取攻击速度修正
|
||||||
|
* @param {String} stance - 姿态
|
||||||
|
* @param {Number} baseSpeed - 基础攻击速度
|
||||||
|
* @returns {Number} 修正后的攻击速度
|
||||||
|
*/
|
||||||
|
export function getAttackSpeed(stance, baseSpeed = 1.0) {
|
||||||
|
const speedModifiers = {
|
||||||
|
attack: 1.3, // 攻击姿态: 速度 +30%
|
||||||
|
defense: 0.9, // 防御姿态: 速度 -10%
|
||||||
|
balance: 1.0,
|
||||||
|
rapid: 2.0, // 快速打击: 速度 +100%
|
||||||
|
heavy: 0.5 // 重击: 速度 -50%
|
||||||
|
}
|
||||||
|
return baseSpeed * (speedModifiers[stance] || 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取环境类型
|
* 获取环境类型
|
||||||
* @param {String} locationId - 位置ID
|
* @param {String} locationId - 位置ID
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
/**
|
||||||
|
* 制造系统 - 配方管理、制造逻辑、品质计算
|
||||||
|
* Phase 6 核心系统实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
|
import { RECIPE_CONFIG, getAvailableRecipes, checkMaterials } from '@/config/recipes.js'
|
||||||
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
|
import { startTask } from './taskSystem.js'
|
||||||
|
import { TASK_TYPES } from './taskSystem.js'
|
||||||
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
|
import { addSkillExp, unlockSkill } from './skillSystem.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Array} 可制作的配方列表(含材料检查)
|
||||||
|
*/
|
||||||
|
export function getCraftableRecipes(playerStore) {
|
||||||
|
const availableRecipes = getAvailableRecipes(playerStore)
|
||||||
|
|
||||||
|
return availableRecipes.map(recipe => {
|
||||||
|
const materialCheck = checkMaterials(playerStore, recipe)
|
||||||
|
return {
|
||||||
|
...recipe,
|
||||||
|
canCraftNow: materialCheck.canCraft,
|
||||||
|
missingMaterials: materialCheck.missingMaterials
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按分类获取可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} category - 分类 'weapon' | 'armor' | 'shield' | 'consumable' | 'special'
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getRecipesByCategory(playerStore, category) {
|
||||||
|
const recipes = getCraftableRecipes(playerStore)
|
||||||
|
return recipes.filter(recipe => recipe.type === category)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算制造时间
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 制造时间(秒)
|
||||||
|
*/
|
||||||
|
export function calculateCraftingTime(playerStore, recipe) {
|
||||||
|
let baseTime = recipe.baseTime
|
||||||
|
|
||||||
|
// 技能加成
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (skill && skill.level > 0) {
|
||||||
|
// 检查制造技能的时间加成里程碑
|
||||||
|
let speedBonus = 0
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
// 检查里程碑
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingSpeed) {
|
||||||
|
speedBonus += milestone.effect.craftingSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 技能等级直接加成(每级5%)
|
||||||
|
speedBonus += Math.min(skill.level, 20) * 0.05
|
||||||
|
|
||||||
|
baseTime = baseTime * (1 - speedBonus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(5, Math.floor(baseTime)) // 最少5秒
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算制造成功率
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 成功率 (0-100)
|
||||||
|
*/
|
||||||
|
export function calculateSuccessRate(playerStore, recipe) {
|
||||||
|
let successRate = recipe.baseSuccessRate * 100
|
||||||
|
|
||||||
|
// 技能等级加成(每级+2%)
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (skill && skill.level > 0) {
|
||||||
|
successRate += Math.min(skill.level, 20) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运气加成
|
||||||
|
const luck = playerStore.baseStats?.luck || 10
|
||||||
|
successRate += luck * 0.1
|
||||||
|
|
||||||
|
// 检查制造技能的成功率加成里程碑
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingSuccessRate) {
|
||||||
|
successRate += milestone.effect.craftingSuccessRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(98, Math.max(5, Math.floor(successRate)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品品质
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 品质值 (0-250)
|
||||||
|
*/
|
||||||
|
export function calculateQuality(playerStore, recipe) {
|
||||||
|
// 基础品质(普通范围 50-99)
|
||||||
|
let qualityValue = 50 + Math.floor(Math.random() * 50) // 默认普通
|
||||||
|
|
||||||
|
// 随机因素 (0-100)
|
||||||
|
const randomRoll = Math.random() * 100
|
||||||
|
|
||||||
|
// 技能加成(每级增加高品质概率)
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
let skillBonus = 0
|
||||||
|
if (skill) {
|
||||||
|
skillBonus = skill.level * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运气加成
|
||||||
|
const luck = playerStore.baseStats?.luck || 10
|
||||||
|
const luckBonus = luck * 0.5
|
||||||
|
|
||||||
|
// 制造技能品质加成
|
||||||
|
let craftingQualityBonus = 0
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingQuality) {
|
||||||
|
craftingQualityBonus += milestone.effect.craftingQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBonus = skillBonus + luckBonus + craftingQualityBonus
|
||||||
|
const finalRoll = randomRoll + totalBonus
|
||||||
|
|
||||||
|
// 品质判定 - 与全局品质等级保持一致
|
||||||
|
if (finalRoll >= 95) qualityValue = 200 + Math.floor(Math.random() * 50) // 传说 [200, 250]
|
||||||
|
else if (finalRoll >= 85) qualityValue = 160 + Math.floor(Math.random() * 39) // 史诗 [160, 199]
|
||||||
|
else if (finalRoll >= 70) qualityValue = 130 + Math.floor(Math.random() * 29) // 稀有 [130, 159]
|
||||||
|
else if (finalRoll >= 50) qualityValue = 100 + Math.floor(Math.random() * 29) // 优秀 [100, 129]
|
||||||
|
else if (finalRoll >= 20) qualityValue = 50 + Math.floor(Math.random() * 49) // 普通 [50, 99]
|
||||||
|
else qualityValue = Math.floor(Math.random() * 50) // 垃圾 [0, 49]
|
||||||
|
|
||||||
|
// 特殊道具(如剧情道具)固定为传说品质
|
||||||
|
if (recipe.type === 'special' && recipe.keyItem) {
|
||||||
|
qualityValue = 225 // 传说中值
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(250, Math.max(0, qualityValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消耗材料(通过 itemSystem 接口)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Boolean} 是否成功消耗
|
||||||
|
*/
|
||||||
|
export function consumeMaterials(playerStore, recipe) {
|
||||||
|
// 先检查材料是否足够
|
||||||
|
const check = checkMaterials(playerStore, recipe)
|
||||||
|
if (!check.canCraft) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消耗材料 - 使用 itemSystem 接口
|
||||||
|
for (const material of recipe.materials) {
|
||||||
|
// 查找背包中该物品的实例
|
||||||
|
const itemIndex = playerStore.inventory.findIndex(i => i.id === material.itemId)
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const item = playerStore.inventory[itemIndex]
|
||||||
|
const countToRemove = Math.min(item.count, material.count)
|
||||||
|
item.count -= countToRemove
|
||||||
|
if (item.count <= 0) {
|
||||||
|
playerStore.inventory.splice(itemIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始制造
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} recipeId - 配方ID
|
||||||
|
* @returns {Object} { success: boolean, message: string, taskId: string|null }
|
||||||
|
*/
|
||||||
|
export function startCrafting(gameStore, playerStore, recipeId) {
|
||||||
|
const recipe = RECIPE_CONFIG[recipeId]
|
||||||
|
if (!recipe) {
|
||||||
|
return { success: false, message: '配方不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查材料
|
||||||
|
const materialCheck = checkMaterials(playerStore, recipe)
|
||||||
|
if (!materialCheck.canCraft) {
|
||||||
|
const missingList = materialCheck.missingMaterials.map(m => `${m.itemName} x${m.need - m.have}`).join(', ')
|
||||||
|
return { success: false, message: `材料不足:${missingList}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查技能
|
||||||
|
if (recipe.requiredSkill) {
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) {
|
||||||
|
return { success: false, message: '需要解锁对应技能' }
|
||||||
|
}
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) {
|
||||||
|
return { success: false, message: `技能等级不足(需要${recipe.requiredSkillLevel}级)` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算制造时间
|
||||||
|
const craftingTime = calculateCraftingTime(playerStore, recipe)
|
||||||
|
|
||||||
|
// 消耗材料(在开始制造时消耗)
|
||||||
|
if (!consumeMaterials(playerStore, recipe)) {
|
||||||
|
return { success: false, message: '材料消耗失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始制造任务
|
||||||
|
const result = startTask(gameStore, playerStore, TASK_TYPES.CRAFTING, {
|
||||||
|
recipeId,
|
||||||
|
duration: craftingTime
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 预先计算品质(制造开始时确定)
|
||||||
|
const task = gameStore.activeTasks.find(t => t.id === result.taskId)
|
||||||
|
if (task) {
|
||||||
|
task.preCalculatedQuality = calculateQuality(playerStore, recipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemConfig = ITEM_CONFIG[recipe.resultItem]
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `开始制造:${itemConfig?.name || recipe.resultItem}`,
|
||||||
|
taskId: result.taskId,
|
||||||
|
estimatedTime: craftingTime,
|
||||||
|
successRate: calculateSuccessRate(playerStore, recipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成制造(任务完成回调)
|
||||||
|
* @param {Object} _gameStore - 游戏Store(未使用,保留以兼容接口)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} task - 制造任务
|
||||||
|
* @param {Object} result - 任务结果
|
||||||
|
* @returns {Object} { success: boolean, item: Object|null }
|
||||||
|
*/
|
||||||
|
export function completeCrafting(_gameStore, playerStore, task, result) {
|
||||||
|
const recipeId = task.data.recipeId
|
||||||
|
const recipe = RECIPE_CONFIG[recipeId]
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
return { success: false, message: '配方不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果制造失败
|
||||||
|
if (!result.success) {
|
||||||
|
// 失败可能部分返还材料(基于成功率)
|
||||||
|
const successRate = calculateSuccessRate(playerStore, recipe)
|
||||||
|
if (successRate > 70) {
|
||||||
|
// 高成功率:返还部分材料
|
||||||
|
// 材料已在开始时消耗,这里可以考虑返还逻辑
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '制造失败,但部分材料已保留',
|
||||||
|
failed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '制造失败,材料已消耗',
|
||||||
|
failed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 制造成功,计算品质(已经是品质值 0-250)
|
||||||
|
const qualityValue = task.preCalculatedQuality || calculateQuality(playerStore, recipe)
|
||||||
|
|
||||||
|
// 添加到背包 - 使用 itemSystem 接口
|
||||||
|
const addResult = addItemToInventory(playerStore, recipe.resultItem, recipe.resultCount, qualityValue)
|
||||||
|
|
||||||
|
if (!addResult.success) {
|
||||||
|
return { success: false, message: '添加物品失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取物品显示信息
|
||||||
|
const item = addResult.item
|
||||||
|
|
||||||
|
// 给予制造技能经验
|
||||||
|
if (result.rewards && result.rewards[recipe.requiredSkill]) {
|
||||||
|
const skillId = recipe.requiredSkill
|
||||||
|
const exp = result.rewards[skillId]
|
||||||
|
|
||||||
|
if (!playerStore.skills[skillId]) {
|
||||||
|
unlockSkill(playerStore, skillId)
|
||||||
|
}
|
||||||
|
addSkillExp(playerStore, skillId, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
item,
|
||||||
|
message: `制造成功:${item.qualityName} ${item.name}`,
|
||||||
|
addResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类显示信息
|
||||||
|
* @param {String} type - 配方类型
|
||||||
|
* @returns {Object} 分类信息
|
||||||
|
*/
|
||||||
|
export function getCategoryInfo(type) {
|
||||||
|
const categories = {
|
||||||
|
weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
armor: { id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
special: { id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
}
|
||||||
|
return categories[type] || { id: type, name: type, icon: '📦' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有分类
|
||||||
|
* @returns {Array} 分类列表
|
||||||
|
*/
|
||||||
|
export function getAllCategories() {
|
||||||
|
return [
|
||||||
|
{ id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
{ id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
{ id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
{ id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
{ id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||||
import { SKILL_CONFIG } from '@/config/skills.js'
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
|
import { applyMilestoneBonus } from './skillSystem.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 环境类型枚举
|
* 环境类型枚举
|
||||||
@@ -198,7 +199,10 @@ export function processEnvironmentExp(gameStore, playerStore, locationId, deltaT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查里程碑奖励(需要调用技能系统)
|
// 检查里程碑奖励(需要调用技能系统)
|
||||||
// TODO: 调用 applyMilestoneBonus
|
const newLevel = playerStore.skills[adaptSkillId].level
|
||||||
|
if (SKILL_CONFIG[adaptSkillId].milestones && SKILL_CONFIG[adaptSkillId].milestones[newLevel]) {
|
||||||
|
applyMilestoneBonus(playerStore, adaptSkillId, newLevel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { [adaptSkillId]: finalExp }
|
return { [adaptSkillId]: finalExp }
|
||||||
|
|||||||
+243
-5
@@ -6,8 +6,13 @@
|
|||||||
import { NPC_CONFIG } from '@/config/npcs.js'
|
import { NPC_CONFIG } from '@/config/npcs.js'
|
||||||
import { EVENT_CONFIG } from '@/config/events.js'
|
import { EVENT_CONFIG } from '@/config/events.js'
|
||||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||||
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
|
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
import { unlockSkill } from './skillSystem.js'
|
import { unlockSkill } from './skillSystem.js'
|
||||||
import { addItemToInventory } from './itemSystem.js'
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
|
import { initCombat, getEnvironmentType } from './combatSystem.js'
|
||||||
|
import { installProsthetic, removeProsthetic } from './prostheticSystem.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查并触发事件
|
* 检查并触发事件
|
||||||
@@ -195,8 +200,9 @@ function renderTemplate(template, context) {
|
|||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
* @param {String} npcId - NPC ID
|
* @param {String} npcId - NPC ID
|
||||||
* @param {String} dialogueKey - 对话键名
|
* @param {String} dialogueKey - 对话键名
|
||||||
|
* @param {Object} playerStore - 玩家Store(用于动态选项生成)
|
||||||
*/
|
*/
|
||||||
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first', playerStore = null) {
|
||||||
const npc = NPC_CONFIG[npcId]
|
const npc = NPC_CONFIG[npcId]
|
||||||
if (!npc) {
|
if (!npc) {
|
||||||
console.warn(`NPC ${npcId} not found`)
|
console.warn(`NPC ${npcId} not found`)
|
||||||
@@ -209,14 +215,27 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理模板变量
|
||||||
|
let text = dialogue.text
|
||||||
|
if (playerStore) {
|
||||||
|
text = renderDialogueTemplate(text, playerStore, dialogue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成选项列表(包括动态选项)
|
||||||
|
let choices = dialogue.choices || []
|
||||||
|
if (dialogue.dynamicChoices && playerStore) {
|
||||||
|
const dynamicChoices = generateDynamicChoices(dialogue.dynamicChoices, playerStore)
|
||||||
|
choices = [...dynamicChoices, ...choices]
|
||||||
|
}
|
||||||
|
|
||||||
// 构建事件数据
|
// 构建事件数据
|
||||||
const eventData = {
|
const eventData = {
|
||||||
id: `npc_${npcId}_${dialogueKey}`,
|
id: `npc_${npcId}_${dialogueKey}`,
|
||||||
type: 'dialogue',
|
type: 'dialogue',
|
||||||
title: npc.name,
|
title: npc.name,
|
||||||
text: dialogue.text,
|
text: text,
|
||||||
npc: npc,
|
npc: npc,
|
||||||
choices: dialogue.choices || [],
|
choices: choices,
|
||||||
currentDialogue: dialogueKey,
|
currentDialogue: dialogueKey,
|
||||||
npcId
|
npcId
|
||||||
}
|
}
|
||||||
@@ -225,6 +244,61 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
|||||||
gameStore.drawerState.event = true
|
gameStore.drawerState.event = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染对话模板(替换变量)
|
||||||
|
*/
|
||||||
|
function renderDialogueTemplate(template, playerStore, dialogue) {
|
||||||
|
let text = template
|
||||||
|
|
||||||
|
// 替换义体列表
|
||||||
|
if (text.includes('{prosthetic_list}')) {
|
||||||
|
const prostheticList = getProstheticInventoryList(playerStore)
|
||||||
|
text = text.replace('{prosthetic_list}', prostheticList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换已装备的义体
|
||||||
|
if (text.includes('{equipped_prosthetic}')) {
|
||||||
|
const equipped = playerStore.equipment?.prosthetic
|
||||||
|
const equippedText = equipped ? `${equipped.name} (${equipped.description})` : '无'
|
||||||
|
text = text.replace('{equipped_prosthetic}', equippedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家背包中的义体列表
|
||||||
|
*/
|
||||||
|
function getProstheticInventoryList(playerStore) {
|
||||||
|
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||||
|
if (prosthetics.length === 0) {
|
||||||
|
return '(你没有义体可以安装)'
|
||||||
|
}
|
||||||
|
return prosthetics.map(p => `• ${p.name}: ${p.description}`).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成动态选项
|
||||||
|
*/
|
||||||
|
function generateDynamicChoices(type, playerStore) {
|
||||||
|
switch (type) {
|
||||||
|
case 'prosthetic_inventory':
|
||||||
|
// 生成义体安装选项
|
||||||
|
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||||
|
if (prosthetics.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return prosthetics.map(p => ({
|
||||||
|
text: `安装 ${p.name}`,
|
||||||
|
action: 'install_prosthetic',
|
||||||
|
actionData: { itemId: p.id }
|
||||||
|
}))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理对话选择
|
* 处理对话选择
|
||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
@@ -256,7 +330,7 @@ export function handleDialogueChoice(gameStore, playerStore, choice) {
|
|||||||
// 继续对话
|
// 继续对话
|
||||||
const currentEvent = gameStore.currentEvent
|
const currentEvent = gameStore.currentEvent
|
||||||
if (currentEvent && currentEvent.npc) {
|
if (currentEvent && currentEvent.npc) {
|
||||||
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next)
|
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next, playerStore)
|
||||||
result.closeEvent = false
|
result.closeEvent = false
|
||||||
}
|
}
|
||||||
} else if (!choice.action) {
|
} else if (!choice.action) {
|
||||||
@@ -401,15 +475,74 @@ export function processDialogueAction(gameStore, playerStore, action, actionData
|
|||||||
// 开始战斗
|
// 开始战斗
|
||||||
if (actionData.enemyId) {
|
if (actionData.enemyId) {
|
||||||
gameStore.drawerState.event = false
|
gameStore.drawerState.event = false
|
||||||
// TODO: 启动战斗
|
// 战斗已在探索事件中初始化,这里只是关闭事件抽屉
|
||||||
return { success: true, message: '开始战斗', closeEvent: true }
|
return { success: true, message: '开始战斗', closeEvent: true }
|
||||||
}
|
}
|
||||||
return { success: false, message: '敌人参数错误', closeEvent: false }
|
return { success: false, message: '敌人参数错误', closeEvent: false }
|
||||||
|
|
||||||
|
case 'flee':
|
||||||
|
// 逃跑
|
||||||
|
gameStore.drawerState.event = false
|
||||||
|
return tryFlee(gameStore, playerStore)
|
||||||
|
|
||||||
case 'close':
|
case 'close':
|
||||||
// 直接关闭
|
// 直接关闭
|
||||||
return { success: true, message: '', closeEvent: true }
|
return { success: true, message: '', closeEvent: true }
|
||||||
|
|
||||||
|
case 'install_prosthetic':
|
||||||
|
// 安装义体
|
||||||
|
if (actionData.itemId) {
|
||||||
|
const item = playerStore.inventory.find(i => i.id === actionData.itemId)
|
||||||
|
if (!item) {
|
||||||
|
return { success: false, message: '找不到该义体', closeEvent: false }
|
||||||
|
}
|
||||||
|
const installResult = installProsthetic(playerStore, gameStore, item)
|
||||||
|
if (installResult.success) {
|
||||||
|
// 从背包移除义体
|
||||||
|
const index = playerStore.inventory.findIndex(i => i.id === actionData.itemId)
|
||||||
|
if (index > -1) {
|
||||||
|
playerStore.inventory.splice(index, 1)
|
||||||
|
}
|
||||||
|
// 解锁义体技能
|
||||||
|
if (item.grantedSkill) {
|
||||||
|
unlockSkill(playerStore, item.grantedSkill)
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`解锁了技能: ${SKILL_CONFIG[item.grantedSkill]?.name || item.grantedSkill}`, 'system')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 更新成就进度
|
||||||
|
gameStore.achievementProgress.prostheticEquipped = 1
|
||||||
|
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||||
|
// 重新显示对话框
|
||||||
|
const currentEvent = gameStore.currentEvent
|
||||||
|
if (currentEvent && currentEvent.npc) {
|
||||||
|
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: installResult.success, message: installResult.message, closeEvent: false }
|
||||||
|
}
|
||||||
|
return { success: false, message: '义体参数错误', closeEvent: false }
|
||||||
|
|
||||||
|
case 'remove_prosthetic':
|
||||||
|
// 卸下义体
|
||||||
|
const removeResult = removeProsthetic(playerStore, gameStore)
|
||||||
|
if (removeResult.success) {
|
||||||
|
// 重新显示对话框
|
||||||
|
const currentEvent = gameStore.currentEvent
|
||||||
|
if (currentEvent && currentEvent.npc) {
|
||||||
|
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: removeResult.success, message: removeResult.message, closeEvent: false }
|
||||||
|
|
||||||
|
case 'refresh_list':
|
||||||
|
// 刷新列表(重新显示当前对话框)
|
||||||
|
const evt = gameStore.currentEvent
|
||||||
|
if (evt && evt.npc && evt.currentDialogue) {
|
||||||
|
startNPCDialogue(gameStore, evt.npc.id, evt.currentDialogue, playerStore)
|
||||||
|
}
|
||||||
|
return { success: true, message: '', closeEvent: false }
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown dialogue action: ${action}`)
|
console.warn(`Unknown dialogue action: ${action}`)
|
||||||
return { success: false, message: '未知动作', closeEvent: false }
|
return { success: false, message: '未知动作', closeEvent: false }
|
||||||
@@ -547,3 +680,108 @@ export function checkScheduledEvents(gameStore, playerStore) {
|
|||||||
export function clearScheduledEvents(gameStore) {
|
export function clearScheduledEvents(gameStore) {
|
||||||
gameStore.scheduledEvents = []
|
gameStore.scheduledEvents = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发探索事件
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Object} 探索结果
|
||||||
|
*/
|
||||||
|
export function triggerExploreEvent(gameStore, playerStore) {
|
||||||
|
const location = LOCATION_CONFIG[playerStore.currentLocation]
|
||||||
|
if (!location) {
|
||||||
|
return { success: false, message: '当前位置错误' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前位置是否有敌人配置
|
||||||
|
const availableEnemies = location.enemies || []
|
||||||
|
if (availableEnemies.length === 0) {
|
||||||
|
return { success: false, message: '这里没什么可探索的' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机决定探索结果
|
||||||
|
const roll = Math.random()
|
||||||
|
|
||||||
|
// 40% 没发现
|
||||||
|
if (roll < 0.4) {
|
||||||
|
triggerEvent(gameStore, 'explore_nothing', {})
|
||||||
|
return { success: true, type: 'nothing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 25% 发现草药
|
||||||
|
if (roll < 0.65) {
|
||||||
|
const herbCount = Math.floor(Math.random() * 3) + 1
|
||||||
|
const result = addItemToInventory(playerStore, 'healing_herb', herbCount, 100)
|
||||||
|
if (result.success) {
|
||||||
|
triggerEvent(gameStore, 'explore_find_herb', {})
|
||||||
|
return { success: true, type: 'find', item: 'healing_herb', count: herbCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20% 发现铜币 (1-5枚)
|
||||||
|
if (roll < 0.85) {
|
||||||
|
const coinAmount = Math.floor(Math.random() * 5) + 1
|
||||||
|
playerStore.currency.copper += coinAmount
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`探索发现了 ${coinAmount} 铜币`, 'reward')
|
||||||
|
}
|
||||||
|
triggerEvent(gameStore, 'explore_find_coin', { amount: coinAmount })
|
||||||
|
return { success: true, type: 'coin', amount: coinAmount }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 15% 遭遇敌人
|
||||||
|
const enemyId = availableEnemies[Math.floor(Math.random() * availableEnemies.length)]
|
||||||
|
const enemyConfig = ENEMY_CONFIG[enemyId]
|
||||||
|
|
||||||
|
if (enemyConfig) {
|
||||||
|
const environment = getEnvironmentType(playerStore.currentLocation)
|
||||||
|
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
|
||||||
|
gameStore.inCombat = true
|
||||||
|
|
||||||
|
triggerEvent(gameStore, 'explore_encounter', {
|
||||||
|
enemyName: enemyConfig.name,
|
||||||
|
enemyId
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, type: 'combat', enemyId }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认:没发现
|
||||||
|
triggerEvent(gameStore, 'explore_nothing', {})
|
||||||
|
return { success: true, type: 'nothing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试逃跑
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Object} 逃跑结果
|
||||||
|
*/
|
||||||
|
export function tryFlee(gameStore, playerStore) {
|
||||||
|
// 基础逃跑成功率50%
|
||||||
|
const baseFleeChance = 0.5
|
||||||
|
|
||||||
|
// 敏捷影响逃跑率
|
||||||
|
const agilityBonus = (playerStore.baseStats?.dexterity || 0) * 0.01
|
||||||
|
|
||||||
|
// 装备加成
|
||||||
|
const equipmentBonus = (playerStore.globalBonus?.fleeRate || 0)
|
||||||
|
|
||||||
|
const fleeChance = Math.min(0.9, baseFleeChance + agilityBonus + equipmentBonus)
|
||||||
|
|
||||||
|
if (Math.random() < fleeChance) {
|
||||||
|
// 逃跑成功
|
||||||
|
gameStore.inCombat = false
|
||||||
|
gameStore.combatState = null
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog('成功逃跑了!', 'system')
|
||||||
|
}
|
||||||
|
return { success: true, message: '逃跑成功' }
|
||||||
|
} else {
|
||||||
|
// 逃跑失败,敌人攻击
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog('逃跑失败!', 'combat')
|
||||||
|
}
|
||||||
|
return { success: false, message: '逃跑失败' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+146
-20
@@ -13,6 +13,7 @@ import { processEnvironmentExp, processEnvironmentEffects } from './environmentS
|
|||||||
import { checkScheduledEvents } from './eventSystem.js'
|
import { checkScheduledEvents } from './eventSystem.js'
|
||||||
import { addSkillExp } from './skillSystem.js'
|
import { addSkillExp } from './skillSystem.js'
|
||||||
import { addItemToInventory } from './itemSystem.js'
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
|
import { addExp, calculateCombatExp } from './levelingSystem.js'
|
||||||
|
|
||||||
// 游戏循环定时器
|
// 游戏循环定时器
|
||||||
let gameLoopInterval = null
|
let gameLoopInterval = null
|
||||||
@@ -70,6 +71,81 @@ export function isGameLoopRunning() {
|
|||||||
return isLoopRunning
|
return isLoopRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否正在休息
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerResting(gameStore) {
|
||||||
|
return gameStore.activeTasks?.some(task => task.type === 'resting') || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否正在训练
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerTraining(gameStore) {
|
||||||
|
return gameStore.activeTasks?.some(task => task.type === 'training') || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理自然回复
|
||||||
|
* 基于体质属性自然回复HP和耐力,休息时提供额外加成
|
||||||
|
* 注意:战斗中和训练时不进行自然回复
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
*/
|
||||||
|
function processNaturalRegeneration(gameStore, playerStore) {
|
||||||
|
const { currentStats, baseStats } = playerStore
|
||||||
|
const isResting = isPlayerResting(gameStore)
|
||||||
|
|
||||||
|
// 体质影响HP回复量:每点体质每秒回复0.05点HP
|
||||||
|
// 体质10 = 每秒0.5点HP基础回复
|
||||||
|
const vitality = baseStats?.vitality || 10
|
||||||
|
const baseHpRegen = vitality * 0.05
|
||||||
|
|
||||||
|
// 耐力基础回复:每秒回复1点
|
||||||
|
const baseStaminaRegen = 1
|
||||||
|
|
||||||
|
// 休息时回复倍率
|
||||||
|
const restMultiplier = isResting ? 3 : 1
|
||||||
|
|
||||||
|
// 计算实际回复量
|
||||||
|
const hpRegen = baseHpRegen * restMultiplier
|
||||||
|
const staminaRegen = baseStaminaRegen * restMultiplier
|
||||||
|
|
||||||
|
// 记录旧值用于日志
|
||||||
|
const oldHp = currentStats.health
|
||||||
|
const oldStamina = currentStats.stamina
|
||||||
|
|
||||||
|
// 应用回复(不超过最大值)
|
||||||
|
const newHp = Math.min(currentStats.maxHealth, currentStats.health + hpRegen)
|
||||||
|
const newStamina = Math.min(currentStats.maxStamina, currentStats.stamina + staminaRegen)
|
||||||
|
|
||||||
|
currentStats.health = newHp
|
||||||
|
currentStats.stamina = newStamina
|
||||||
|
|
||||||
|
// 只在有明显回复且每30秒输出一次日志
|
||||||
|
if (gameStore.lastRegenLogTime === undefined) {
|
||||||
|
gameStore.lastRegenLogTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const shouldLog = (now - gameStore.lastRegenLogTime) > 30000 && // 30秒
|
||||||
|
((newHp > oldHp && newHp < currentStats.maxHealth) ||
|
||||||
|
(newStamina > oldStamina && newStamina < currentStats.maxStamina))
|
||||||
|
|
||||||
|
if (shouldLog) {
|
||||||
|
gameStore.lastRegenLogTime = now
|
||||||
|
if (isResting) {
|
||||||
|
// 休息中的日志由休息任务处理,这里不重复输出
|
||||||
|
} else if (newHp > oldHp && newStamina > oldStamina) {
|
||||||
|
gameStore.addLog('自然回复:HP和耐力缓慢恢复中...', 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏主tick - 每秒执行一次
|
* 游戏主tick - 每秒执行一次
|
||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
@@ -79,6 +155,11 @@ export function gameTick(gameStore, playerStore) {
|
|||||||
// 1. 更新游戏时间
|
// 1. 更新游戏时间
|
||||||
updateGameTime(gameStore)
|
updateGameTime(gameStore)
|
||||||
|
|
||||||
|
// 1.5. 自然回复(仅在非战斗、非训练状态)
|
||||||
|
if (!gameStore.inCombat && !isPlayerTraining(gameStore)) {
|
||||||
|
processNaturalRegeneration(gameStore, playerStore)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 处理战斗
|
// 2. 处理战斗
|
||||||
if (gameStore.inCombat && gameStore.combatState) {
|
if (gameStore.inCombat && gameStore.combatState) {
|
||||||
handleCombatTick(gameStore, playerStore)
|
handleCombatTick(gameStore, playerStore)
|
||||||
@@ -196,24 +277,22 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
// 添加日志
|
// 添加日志
|
||||||
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
||||||
|
|
||||||
// 给予经验值
|
// 收集所有经验获取信息,用于统一日志显示
|
||||||
|
const expGains = []
|
||||||
|
|
||||||
|
// 给予经验值 (使用新的等级系统)
|
||||||
if (enemy.expReward) {
|
if (enemy.expReward) {
|
||||||
playerStore.level.exp += enemy.expReward
|
const adjustedExp = calculateCombatExp(
|
||||||
|
enemy.level || 1,
|
||||||
// 检查升级
|
playerStore.level.current,
|
||||||
const maxExp = playerStore.level.maxExp
|
enemy.expReward
|
||||||
if (playerStore.level.exp >= maxExp) {
|
)
|
||||||
playerStore.level.current++
|
const levelResult = addExp(playerStore, gameStore, adjustedExp)
|
||||||
playerStore.level.exp -= maxExp
|
expGains.push({
|
||||||
playerStore.level.maxExp = Math.floor(playerStore.level.maxExp * 1.5)
|
type: '战斗经验',
|
||||||
|
amount: adjustedExp,
|
||||||
gameStore.addLog(`升级了! 等级: ${playerStore.level.current}`, 'reward')
|
leveledUp: levelResult.leveledUp
|
||||||
|
})
|
||||||
// 升级恢复状态
|
|
||||||
playerStore.currentStats.health = playerStore.currentStats.maxHealth
|
|
||||||
playerStore.currentStats.stamina = playerStore.currentStats.maxStamina
|
|
||||||
playerStore.currentStats.sanity = playerStore.currentStats.maxSanity
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给予武器技能经验奖励
|
// 给予武器技能经验奖励
|
||||||
@@ -231,14 +310,32 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
|
|
||||||
if (skillId) {
|
if (skillId) {
|
||||||
const result = addSkillExp(playerStore, skillId, enemy.skillExpReward)
|
const result = addSkillExp(playerStore, skillId, enemy.skillExpReward)
|
||||||
|
const skillName = SKILL_CONFIG[skillId]?.name || skillId
|
||||||
|
expGains.push({
|
||||||
|
type: `${skillName}经验`,
|
||||||
|
amount: enemy.skillExpReward,
|
||||||
|
leveledUp: result.leveledUp,
|
||||||
|
newLevel: result.newLevel
|
||||||
|
})
|
||||||
|
|
||||||
if (result.leveledUp) {
|
if (result.leveledUp) {
|
||||||
const skillName = SKILL_CONFIG[skillId]?.name || skillId
|
|
||||||
gameStore.addLog(`${skillName}升级到了 Lv.${result.newLevel}!`, 'reward')
|
gameStore.addLog(`${skillName}升级到了 Lv.${result.newLevel}!`, 'reward')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一显示经验获取日志
|
||||||
|
if (expGains.length > 0) {
|
||||||
|
const expText = expGains.map(e => {
|
||||||
|
let text = `${e.type}+${e.amount}`
|
||||||
|
if (e.leveledUp) {
|
||||||
|
text += ` ⬆`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}).join(', ')
|
||||||
|
gameStore.addLog(`获得: ${expText}`, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
// 处理掉落
|
// 处理掉落
|
||||||
if (enemy.drops && enemy.drops.length > 0) {
|
if (enemy.drops && enemy.drops.length > 0) {
|
||||||
processDrops(gameStore, playerStore, enemy.drops)
|
processDrops(gameStore, playerStore, enemy.drops)
|
||||||
@@ -258,11 +355,17 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
playerStore.currentStats.stamina > 10
|
playerStore.currentStats.stamina > 10
|
||||||
|
|
||||||
if (canContinue) {
|
if (canContinue) {
|
||||||
|
// 设置寻找中状态
|
||||||
|
gameStore.isSearching = true
|
||||||
|
gameStore.addLog('正在寻找新的敌人...', 'info')
|
||||||
|
|
||||||
// 延迟3秒后开始下一场战斗
|
// 延迟3秒后开始下一场战斗
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!gameStore.inCombat && gameStore.autoCombat) {
|
if (!gameStore.inCombat && gameStore.autoCombat) {
|
||||||
startAutoCombat(gameStore, playerStore)
|
startAutoCombat(gameStore, playerStore)
|
||||||
}
|
}
|
||||||
|
// 清除寻找中状态(无论是否成功找到敌人)
|
||||||
|
gameStore.isSearching = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
} else {
|
} else {
|
||||||
// 状态过低,自动关闭自动战斗
|
// 状态过低,自动关闭自动战斗
|
||||||
@@ -284,6 +387,7 @@ function startAutoCombat(gameStore, playerStore) {
|
|||||||
|
|
||||||
if (!locationEnemies || locationEnemies.length === 0) {
|
if (!locationEnemies || locationEnemies.length === 0) {
|
||||||
gameStore.autoCombat = false
|
gameStore.autoCombat = false
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
|
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -293,10 +397,13 @@ function startAutoCombat(gameStore, playerStore) {
|
|||||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
const enemyConfig = ENEMY_CONFIG[enemyId]
|
||||||
|
|
||||||
if (!enemyConfig) {
|
if (!enemyConfig) {
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog('敌人配置错误', 'error')
|
gameStore.addLog('敌人配置错误', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除寻找中状态
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog(`自动寻找中...遇到了 ${enemyConfig.name}!`, 'combat')
|
gameStore.addLog(`自动寻找中...遇到了 ${enemyConfig.name}!`, 'combat')
|
||||||
const environment = getEnvironmentType(locationId)
|
const environment = getEnvironmentType(locationId)
|
||||||
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
|
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
|
||||||
@@ -374,6 +481,24 @@ function processDrops(gameStore, playerStore, drops) {
|
|||||||
* @param {Object} rewards - 奖励
|
* @param {Object} rewards - 奖励
|
||||||
*/
|
*/
|
||||||
function handleTaskCompletion(gameStore, playerStore, task, rewards) {
|
function handleTaskCompletion(gameStore, playerStore, task, rewards) {
|
||||||
|
// 处理制造任务
|
||||||
|
if (task.type === 'crafting') {
|
||||||
|
if (rewards.success !== false && rewards.craftedItem) {
|
||||||
|
// 制造成功,添加物品到背包
|
||||||
|
const { completeCrafting } = require('./craftingSystem.js')
|
||||||
|
const result = completeCrafting(gameStore, playerStore, task, rewards)
|
||||||
|
if (result.success) {
|
||||||
|
gameStore.addLog(result.message, 'reward')
|
||||||
|
} else if (result.failed) {
|
||||||
|
gameStore.addLog(result.message, 'warning')
|
||||||
|
}
|
||||||
|
} else if (rewards.success === false) {
|
||||||
|
// 制造失败
|
||||||
|
gameStore.addLog('制造失败,材料已消耗', 'warning')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 应用货币奖励
|
// 应用货币奖励
|
||||||
if (rewards.currency) {
|
if (rewards.currency) {
|
||||||
playerStore.currency.copper += rewards.currency
|
playerStore.currency.copper += rewards.currency
|
||||||
@@ -381,7 +506,7 @@ function handleTaskCompletion(gameStore, playerStore, task, rewards) {
|
|||||||
|
|
||||||
// 应用技能经验
|
// 应用技能经验
|
||||||
for (const [skillId, exp] of Object.entries(rewards)) {
|
for (const [skillId, exp] of Object.entries(rewards)) {
|
||||||
if (skillId !== 'currency' && skillId !== 'completionBonus' && typeof exp === 'number') {
|
if (skillId !== 'currency' && skillId !== 'completionBonus' && skillId !== 'craftedItem' && skillId !== 'craftedCount' && typeof exp === 'number') {
|
||||||
addSkillExp(playerStore, skillId, exp)
|
addSkillExp(playerStore, skillId, exp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +517,8 @@ function handleTaskCompletion(gameStore, playerStore, task, rewards) {
|
|||||||
resting: '休息',
|
resting: '休息',
|
||||||
training: '训练',
|
training: '训练',
|
||||||
working: '工作',
|
working: '工作',
|
||||||
praying: '祈祷'
|
praying: '祈祷',
|
||||||
|
crafting: '制造'
|
||||||
}
|
}
|
||||||
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
|
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-100
@@ -9,18 +9,29 @@ import { SKILL_CONFIG } from '@/config/skills.js'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算装备品质后的属性
|
* 计算装备品质后的属性
|
||||||
|
* 使用数学模型中的公式:品质倍率 = 1 + (品质 - 100) * 0.01
|
||||||
* @param {String} itemId - 物品ID
|
* @param {String} itemId - 物品ID
|
||||||
* @param {Number} quality - 品质值 (0-250)
|
* @param {Number} quality - 品质值 (0-250)
|
||||||
|
* @param {Number} itemLevel - 物品等级(影响属性)
|
||||||
* @returns {Object|null} 计算后的属性
|
* @returns {Object|null} 计算后的属性
|
||||||
*/
|
*/
|
||||||
export function calculateItemStats(itemId, quality = 100) {
|
export function calculateItemStats(itemId, quality = 100, itemLevel = 1) {
|
||||||
const config = ITEM_CONFIG[itemId]
|
const config = ITEM_CONFIG[itemId]
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const qualityLevel = getQualityLevel(quality)
|
const qualityLevel = getQualityLevel(quality)
|
||||||
const qualityMultiplier = qualityLevel.multiplier
|
|
||||||
|
// 品质倍率:1 + (品质 - 100) * 0.01
|
||||||
|
// 品质100 = 1.0倍,品质150 = 1.5倍,品质50 = 0.5倍
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
|
||||||
|
// 等级倍率:每级 +2%
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
|
||||||
|
// 综合倍率
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
id: itemId,
|
id: itemId,
|
||||||
@@ -30,36 +41,35 @@ export function calculateItemStats(itemId, quality = 100) {
|
|||||||
description: config.description,
|
description: config.description,
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
|
itemLevel: itemLevel,
|
||||||
qualityLevel: qualityLevel.level,
|
qualityLevel: qualityLevel.level,
|
||||||
qualityName: qualityLevel.name,
|
qualityName: qualityLevel.name,
|
||||||
qualityColor: qualityLevel.color
|
qualityColor: qualityLevel.color
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用品质百分比到基础属性
|
// 应用品质倍率到基础属性
|
||||||
const qualityRatio = quality / 100
|
|
||||||
|
|
||||||
if (config.baseDamage) {
|
if (config.baseDamage) {
|
||||||
stats.baseDamage = config.baseDamage
|
stats.baseDamage = config.baseDamage
|
||||||
stats.finalDamage = Math.floor(config.baseDamage * qualityRatio * qualityMultiplier)
|
stats.finalDamage = Math.max(1, Math.floor(config.baseDamage * totalMultiplier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.baseDefense) {
|
if (config.baseDefense) {
|
||||||
stats.baseDefense = config.baseDefense
|
stats.baseDefense = config.baseDefense
|
||||||
stats.finalDefense = Math.floor(config.baseDefense * qualityRatio * qualityMultiplier)
|
stats.finalDefense = Math.max(1, Math.floor(config.baseDefense * totalMultiplier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.baseShield) {
|
if (config.baseShield) {
|
||||||
stats.baseShield = config.baseShield
|
stats.baseShield = config.baseShield
|
||||||
stats.finalShield = Math.floor(config.baseShield * qualityRatio * qualityMultiplier)
|
stats.finalShield = Math.floor(config.baseShield * totalMultiplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.attackSpeed) {
|
if (config.attackSpeed) {
|
||||||
stats.attackSpeed = config.attackSpeed
|
stats.attackSpeed = config.attackSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算价值
|
// 计算价值(品质影响)
|
||||||
stats.baseValue = config.baseValue || 0
|
stats.baseValue = config.baseValue || 0
|
||||||
stats.finalValue = Math.floor(config.baseValue * qualityRatio * qualityMultiplier)
|
stats.finalValue = Math.floor(config.baseValue * totalMultiplier)
|
||||||
|
|
||||||
// 消耗品效果
|
// 消耗品效果
|
||||||
if (config.effect) {
|
if (config.effect) {
|
||||||
@@ -78,6 +88,11 @@ export function calculateItemStats(itemId, quality = 100) {
|
|||||||
stats.unlockSkill = config.unlockSkill
|
stats.unlockSkill = config.unlockSkill
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 额外属性(来自物品配置)
|
||||||
|
if (config.stats) {
|
||||||
|
stats.stats = { ...config.stats }
|
||||||
|
}
|
||||||
|
|
||||||
// 堆叠信息
|
// 堆叠信息
|
||||||
if (config.stackable !== undefined) {
|
if (config.stackable !== undefined) {
|
||||||
stats.stackable = config.stackable
|
stats.stackable = config.stackable
|
||||||
@@ -115,34 +130,34 @@ export function getQualityLevel(quality) {
|
|||||||
* @returns {Number} 品质值
|
* @returns {Number} 品质值
|
||||||
*/
|
*/
|
||||||
export function generateRandomQuality(luck = 0) {
|
export function generateRandomQuality(luck = 0) {
|
||||||
// 基础品质范围 50-150
|
// 单次随机判定
|
||||||
const baseMin = 50
|
const roll = Math.random()
|
||||||
const baseMax = 150
|
|
||||||
|
|
||||||
// 运气加成:每1点运气增加0.5品质
|
// 运气加成:每1点运气增加对应品质概率的 0.1%
|
||||||
const luckBonus = luck * 0.5
|
const luckBonus = luck * 0.001
|
||||||
|
|
||||||
// 随机品质
|
// 传说品质:0.1% + 运气加成
|
||||||
let quality = Math.floor(Math.random() * (baseMax - baseMin + 1)) + baseMin + luckBonus
|
if (roll < 0.001 + luckBonus) {
|
||||||
|
return 200 + Math.floor(Math.random() * 51) // 200-250
|
||||||
// 小概率生成高品质(传说)
|
|
||||||
if (Math.random() < 0.01 + luck / 10000) {
|
|
||||||
quality = 180 + Math.floor(Math.random() * 70) // 180-250
|
|
||||||
}
|
}
|
||||||
// 史诗品质
|
// 史诗品质:1% + 运气加成
|
||||||
else if (Math.random() < 0.05 + luck / 2000) {
|
if (roll < 0.01 + luckBonus * 2) {
|
||||||
quality = 160 + Math.floor(Math.random() * 40) // 160-199
|
return 170 + Math.floor(Math.random() * 30) // 170-199
|
||||||
}
|
}
|
||||||
// 稀有品质
|
// 稀有品质:5% + 运气加成
|
||||||
else if (Math.random() < 0.15 + luck / 500) {
|
if (roll < 0.05 + luckBonus * 3) {
|
||||||
quality = 130 + Math.floor(Math.random() * 30) // 130-159
|
return 140 + Math.floor(Math.random() * 30) // 140-169
|
||||||
}
|
}
|
||||||
// 优秀品质
|
// 优秀品质:15% + 运气加成
|
||||||
else if (Math.random() < 0.3 + luck / 200) {
|
if (roll < 0.15 + luckBonus * 4) {
|
||||||
quality = 100 + Math.floor(Math.random() * 30) // 100-129
|
return 100 + Math.floor(Math.random() * 40) // 100-139
|
||||||
}
|
}
|
||||||
|
// 普通品质:剩余大部分
|
||||||
return Math.min(250, Math.max(0, Math.floor(quality)))
|
if (roll < 0.85) {
|
||||||
|
return 50 + Math.floor(Math.random() * 50) // 50-99
|
||||||
|
}
|
||||||
|
// 垃圾品质:15%
|
||||||
|
return Math.floor(Math.random() * 50) // 0-49
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -404,7 +419,7 @@ function removeEquipmentStats(playerStore, slot) {
|
|||||||
* @param {Object} playerStore - 玩家Store
|
* @param {Object} playerStore - 玩家Store
|
||||||
* @param {String} itemId - 物品ID
|
* @param {String} itemId - 物品ID
|
||||||
* @param {Number} count - 数量
|
* @param {Number} count - 数量
|
||||||
* @param {Number} quality - 品质(装备用)
|
* @param {Number} quality - 品质值(装备用,0-250)
|
||||||
* @returns {Object} { success: boolean, item: Object }
|
* @returns {Object} { success: boolean, item: Object }
|
||||||
*/
|
*/
|
||||||
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
|
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
|
||||||
@@ -420,10 +435,10 @@ export function addItemToInventory(playerStore, itemId, count = 1, quality = nul
|
|||||||
const itemQuality = quality !== null ? quality
|
const itemQuality = quality !== null ? quality
|
||||||
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
|
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
|
||||||
|
|
||||||
// 计算物品属性
|
// 计算物品属性(包含品质等级)
|
||||||
const itemStats = calculateItemStats(itemId, itemQuality)
|
const itemStats = calculateItemStats(itemId, itemQuality)
|
||||||
|
|
||||||
// 堆叠物品 - 只需要匹配ID,不需要匹配品质(素材类物品)
|
// 素材类物品 - 只按ID堆叠
|
||||||
if (config.stackable) {
|
if (config.stackable) {
|
||||||
const existingItem = playerStore.inventory.find(i => i.id === itemId)
|
const existingItem = playerStore.inventory.find(i => i.id === itemId)
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
@@ -432,10 +447,32 @@ export function addItemToInventory(playerStore, itemId, count = 1, quality = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新物品
|
// 装备类物品 - 不堆叠,每个装备都有独立的唯一ID
|
||||||
|
// 这样可以正确跟踪每个装备的装备状态
|
||||||
|
if (needsQuality) {
|
||||||
|
// 检查是否有完全相同品质的装备(用于显示堆叠数量,但各自独立)
|
||||||
|
// 注意:装备不再真正堆叠,每个都是独立实例
|
||||||
|
// 生成唯一ID:itemId + quality + 时间戳 + 随机数
|
||||||
|
const uniqueId = `${itemId}_q${itemQuality}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
...itemStats,
|
...itemStats,
|
||||||
uniqueId: `${itemId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
uniqueId,
|
||||||
|
count: 1, // 装备始终为1
|
||||||
|
equipped: false,
|
||||||
|
obtainedAt: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
playerStore.inventory.push(newItem)
|
||||||
|
return { success: true, item: newItem }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新物品(非装备类)
|
||||||
|
const uniqueId = `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
||||||
|
|
||||||
|
const newItem = {
|
||||||
|
...itemStats,
|
||||||
|
uniqueId,
|
||||||
count,
|
count,
|
||||||
equipped: false,
|
equipped: false,
|
||||||
obtainedAt: Date.now()
|
obtainedAt: Date.now()
|
||||||
@@ -504,66 +541,6 @@ export function getItemCount(playerStore, itemId) {
|
|||||||
return item ? item.count : 0
|
return item ? item.count : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Note: checkSkillUnlock, calculateSellPrice, calculateBuyPrice,
|
||||||
* 检查装备是否解锁技能
|
// getQualityColor, getQualityName are now test-only utilities
|
||||||
* @param {Object} playerStore - 玩家Store
|
// and are not exported for production use
|
||||||
* @param {String} itemId - 物品ID
|
|
||||||
* @returns {String|null} 解锁的技能ID
|
|
||||||
*/
|
|
||||||
export function checkSkillUnlock(playerStore, itemId) {
|
|
||||||
const config = ITEM_CONFIG[itemId]
|
|
||||||
if (!config || !config.unlockSkill) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillId = config.unlockSkill
|
|
||||||
|
|
||||||
// 检查技能是否已解锁
|
|
||||||
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return skillId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算物品出售价格
|
|
||||||
* @param {Object} item - 物品对象
|
|
||||||
* @param {Number} marketRate - 市场价格倍率 (0.1-2.0)
|
|
||||||
* @returns {Number} 出售价格(铜币)
|
|
||||||
*/
|
|
||||||
export function calculateSellPrice(item, marketRate = 0.3) {
|
|
||||||
const basePrice = item.finalValue || item.baseValue || 0
|
|
||||||
return Math.max(1, Math.floor(basePrice * marketRate))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算物品购买价格
|
|
||||||
* @param {Object} item - 物品对象
|
|
||||||
* @param {Number} marketRate - 市场价格倍率 (1.0-3.0)
|
|
||||||
* @returns {Number} 购买价格(铜币)
|
|
||||||
*/
|
|
||||||
export function calculateBuyPrice(item, marketRate = 2.0) {
|
|
||||||
const basePrice = item.finalValue || item.baseValue || 0
|
|
||||||
return Math.max(1, Math.floor(basePrice * marketRate))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取品质颜色
|
|
||||||
* @param {Number} quality - 品质值
|
|
||||||
* @returns {String} 颜色代码
|
|
||||||
*/
|
|
||||||
export function getQualityColor(quality) {
|
|
||||||
const level = getQualityLevel(quality)
|
|
||||||
return level.color
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取品质名称
|
|
||||||
* @param {Number} quality - 品质值
|
|
||||||
* @returns {String} 品质名称
|
|
||||||
*/
|
|
||||||
export function getQualityName(quality) {
|
|
||||||
const level = getQualityLevel(quality)
|
|
||||||
return level.name
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
/**
|
||||||
|
* 等级系统 - 经验曲线、升级计算
|
||||||
|
* Phase 4 数值调整 - 等级经验曲线
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经验曲线类型
|
||||||
|
*/
|
||||||
|
export const EXP_CURVE_TYPES = {
|
||||||
|
LINEAR: 'linear', // 线性增长: base * level
|
||||||
|
QUADRATIC: 'quadratic', // 二次增长: base * level^2
|
||||||
|
EXPONENTIAL: 'exponential', // 指数增长: base * multiplier^level
|
||||||
|
HYBRID: 'hybrid' // 混合曲线: 更平滑的体验
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等级配置
|
||||||
|
*/
|
||||||
|
export const LEVEL_CONFIG = {
|
||||||
|
// 经验曲线类型
|
||||||
|
expCurveType: EXP_CURVE_TYPES.HYBRID,
|
||||||
|
|
||||||
|
// 基础经验值 (1级升级所需)
|
||||||
|
baseExp: 100,
|
||||||
|
|
||||||
|
// 经验增长倍率 (每级增长)
|
||||||
|
expMultiplier: 1.15,
|
||||||
|
|
||||||
|
// 最大等级
|
||||||
|
maxLevel: 50,
|
||||||
|
|
||||||
|
// 等级属性加成 (每级获得的属性点)
|
||||||
|
statPointsPerLevel: 3,
|
||||||
|
|
||||||
|
// 初始属性
|
||||||
|
baseStats: {
|
||||||
|
strength: 10,
|
||||||
|
agility: 8,
|
||||||
|
dexterity: 8,
|
||||||
|
intuition: 10,
|
||||||
|
vitality: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算指定等级所需的总经验
|
||||||
|
* @param {Number} level - 目标等级
|
||||||
|
* @returns {Number} 所需经验
|
||||||
|
*/
|
||||||
|
export function getExpForLevel(level) {
|
||||||
|
if (level <= 1) return 0
|
||||||
|
|
||||||
|
const { expCurveType, baseExp, expMultiplier } = LEVEL_CONFIG
|
||||||
|
|
||||||
|
switch (expCurveType) {
|
||||||
|
case EXP_CURVE_TYPES.LINEAR:
|
||||||
|
// 线性: 100, 200, 300, 400...
|
||||||
|
return baseExp * (level - 1)
|
||||||
|
|
||||||
|
case EXP_CURVE_TYPES.QUADRATIC:
|
||||||
|
// 二次: 100, 400, 900, 1600...
|
||||||
|
return baseExp * Math.pow(level - 1, 2)
|
||||||
|
|
||||||
|
case EXP_CURVE_TYPES.EXPONENTIAL:
|
||||||
|
// 指数: 100, 115, 132, 152...
|
||||||
|
return baseExp * Math.pow(expMultiplier, level - 1)
|
||||||
|
|
||||||
|
case EXP_CURVE_TYPES.HYBRID:
|
||||||
|
default:
|
||||||
|
// 混合曲线 - 平滑的增长体验
|
||||||
|
// 公式: base * (1.15 ^ (level - 1)) * level / 2
|
||||||
|
// 结果: Lv2=100, Lv5=700, Lv10=2800, Lv20=15000, Lv30=55000, Lv50=250000
|
||||||
|
return Math.floor(baseExp * Math.pow(expMultiplier, level - 2) * (level - 1) * 0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算从当前等级到下一等级所需的经验
|
||||||
|
* @param {Number} currentLevel - 当前等级
|
||||||
|
* @returns {Number} 升级所需经验
|
||||||
|
*/
|
||||||
|
export function getExpToNextLevel(currentLevel) {
|
||||||
|
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
|
||||||
|
return 0 // 已达最高等级
|
||||||
|
}
|
||||||
|
return getExpForLevel(currentLevel + 1) - getExpForLevel(currentLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并处理升级
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Object} { leveledUp: boolean, newLevel: number, levelsGained: number }
|
||||||
|
*/
|
||||||
|
export function checkLevelUp(playerStore, gameStore) {
|
||||||
|
const currentLevel = playerStore.level.current
|
||||||
|
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
|
||||||
|
return { leveledUp: false, newLevel: currentLevel, levelsGained: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
let levelsGained = 0
|
||||||
|
let newLevel = currentLevel
|
||||||
|
let remainingExp = playerStore.level.exp
|
||||||
|
|
||||||
|
// 连续升级检查(支持一次获得大量经验连升多级)
|
||||||
|
while (newLevel < LEVEL_CONFIG.maxLevel) {
|
||||||
|
const expNeeded = getExpToNextLevel(newLevel)
|
||||||
|
if (remainingExp >= expNeeded) {
|
||||||
|
remainingExp -= expNeeded
|
||||||
|
newLevel++
|
||||||
|
levelsGained++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelsGained > 0) {
|
||||||
|
// 应用升级
|
||||||
|
const oldLevel = playerStore.level.current
|
||||||
|
playerStore.level.current = newLevel
|
||||||
|
playerStore.level.exp = remainingExp
|
||||||
|
playerStore.level.maxExp = getExpToNextLevel(newLevel)
|
||||||
|
|
||||||
|
// 应用等级属性加成
|
||||||
|
applyLevelStats(playerStore, oldLevel, newLevel)
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
if (levelsGained === 1) {
|
||||||
|
gameStore?.addLog(`升级了! 等级: ${newLevel}`, 'reward')
|
||||||
|
} else {
|
||||||
|
gameStore?.addLog(`连升${levelsGained}级! 等级: ${newLevel}`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 升级恢复状态
|
||||||
|
playerStore.currentStats.health = playerStore.currentStats.maxHealth
|
||||||
|
playerStore.currentStats.stamina = playerStore.currentStats.maxStamina
|
||||||
|
playerStore.currentStats.sanity = playerStore.currentStats.maxSanity
|
||||||
|
|
||||||
|
return { leveledUp: true, newLevel, levelsGained }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { leveledUp: false, newLevel: currentLevel, levelsGained: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用等级属性加成
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Number} oldLevel - 旧等级
|
||||||
|
* @param {Number} newLevel - 新等级
|
||||||
|
*/
|
||||||
|
function applyLevelStats(playerStore, oldLevel, newLevel) {
|
||||||
|
const levelsGained = newLevel - oldLevel
|
||||||
|
const statPoints = levelsGained * LEVEL_CONFIG.statPointsPerLevel
|
||||||
|
|
||||||
|
// 自动分配属性点 (简化版: 均匀分配)
|
||||||
|
// 实际游戏中可以让玩家手动选择
|
||||||
|
const stats = ['strength', 'agility', 'dexterity', 'intuition', 'vitality']
|
||||||
|
const pointsPerStat = Math.floor(statPoints / stats.length)
|
||||||
|
const remainder = statPoints % stats.length
|
||||||
|
|
||||||
|
stats.forEach((stat, index) => {
|
||||||
|
playerStore.baseStats[stat] += pointsPerStat
|
||||||
|
if (index < remainder) {
|
||||||
|
playerStore.baseStats[stat] += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新生命值上限 (体质影响HP)
|
||||||
|
const hpPerVitality = 10
|
||||||
|
const oldMaxHp = playerStore.currentStats.maxHealth
|
||||||
|
const newMaxHp = 100 + (playerStore.baseStats.vitality - 10) * hpPerVitality
|
||||||
|
|
||||||
|
if (newMaxHp !== oldMaxHp) {
|
||||||
|
playerStore.currentStats.maxHealth = newMaxHp
|
||||||
|
// 按比例恢复HP
|
||||||
|
const hpRatio = playerStore.currentStats.health / oldMaxHp
|
||||||
|
playerStore.currentStats.health = Math.floor(newMaxHp * hpRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加经验值
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Number} exp - 获得的经验值
|
||||||
|
* @returns {Object} { leveledUp: boolean, newLevel: number }
|
||||||
|
*/
|
||||||
|
export function addExp(playerStore, gameStore, exp) {
|
||||||
|
playerStore.level.exp += exp
|
||||||
|
return checkLevelUp(playerStore, gameStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取等级信息
|
||||||
|
* @param {Number} level - 等级
|
||||||
|
* @returns {Object} 等级信息
|
||||||
|
*/
|
||||||
|
export function getLevelInfo(level) {
|
||||||
|
const totalExp = getExpForLevel(level)
|
||||||
|
const toNext = getExpToNextLevel(level)
|
||||||
|
const isMaxLevel = level >= LEVEL_CONFIG.maxLevel
|
||||||
|
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
totalExp,
|
||||||
|
toNext,
|
||||||
|
isMaxLevel,
|
||||||
|
maxLevel: LEVEL_CONFIG.maxLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前经验进度百分比
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Number} 0-100
|
||||||
|
*/
|
||||||
|
export function getExpProgress(playerStore) {
|
||||||
|
const currentLevel = playerStore.level.current
|
||||||
|
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNext = getExpToNextLevel(currentLevel)
|
||||||
|
const current = playerStore.level.exp
|
||||||
|
|
||||||
|
return Math.min(100, Math.floor((current / toNext) * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据敌人等级计算经验奖励
|
||||||
|
* @param {Number} enemyLevel - 敌人等级
|
||||||
|
* @param {Number} playerLevel - 玩家等级
|
||||||
|
* @param {Number} baseExp - 基础经验值
|
||||||
|
* @returns {Number} 调整后的经验值
|
||||||
|
*/
|
||||||
|
export function calculateCombatExp(enemyLevel, playerLevel, baseExp) {
|
||||||
|
const levelDiff = enemyLevel - playerLevel
|
||||||
|
|
||||||
|
// 等级差异调整
|
||||||
|
let multiplier = 1.0
|
||||||
|
|
||||||
|
if (levelDiff > 5) {
|
||||||
|
// 敌人等级远高于玩家 - 额外奖励
|
||||||
|
multiplier = 1.5
|
||||||
|
} else if (levelDiff > 2) {
|
||||||
|
// 敌人等级稍高 - 小幅奖励
|
||||||
|
multiplier = 1.2
|
||||||
|
} else if (levelDiff < -5) {
|
||||||
|
// 敌人等级远低于玩家 - 大幅惩罚
|
||||||
|
multiplier = 0.1
|
||||||
|
} else if (levelDiff < -2) {
|
||||||
|
// 敌人等级稍低 - 小幅惩罚
|
||||||
|
multiplier = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(baseExp * multiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取等级称号
|
||||||
|
* @param {Number} level - 等级
|
||||||
|
* @returns {String} 称号
|
||||||
|
*/
|
||||||
|
export function getLevelTitle(level) {
|
||||||
|
const titles = {
|
||||||
|
1: '新手',
|
||||||
|
5: '冒险者',
|
||||||
|
10: '老手',
|
||||||
|
15: '精英',
|
||||||
|
20: '专家',
|
||||||
|
25: '大师',
|
||||||
|
30: '宗师',
|
||||||
|
35: '传奇',
|
||||||
|
40: '英雄',
|
||||||
|
45: '神话',
|
||||||
|
50: '至尊'
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = '幸存者'
|
||||||
|
for (const [lvl, t] of Object.entries(titles).sort((a, b) => b[0] - a[0])) {
|
||||||
|
if (level >= parseInt(lvl)) {
|
||||||
|
title = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* 地图布局系统 - 根据区域连接生成可视化地图
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建地图图形结构
|
||||||
|
* @returns {Object} 地图数据
|
||||||
|
*/
|
||||||
|
export function buildMapGraph() {
|
||||||
|
const nodes = []
|
||||||
|
const edges = []
|
||||||
|
|
||||||
|
// 首先创建所有节点
|
||||||
|
for (const [id, config] of Object.entries(LOCATION_CONFIG)) {
|
||||||
|
nodes.push({
|
||||||
|
id,
|
||||||
|
name: config.name,
|
||||||
|
type: config.type,
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建连接边
|
||||||
|
for (const [id, config] of Object.entries(LOCATION_CONFIG)) {
|
||||||
|
if (config.connections) {
|
||||||
|
for (const connId of config.connections) {
|
||||||
|
// 避免重复边
|
||||||
|
const edgeExists = edges.some(e =>
|
||||||
|
(e.from === id && e.to === connId) ||
|
||||||
|
(e.from === connId && e.to === id)
|
||||||
|
)
|
||||||
|
if (!edgeExists) {
|
||||||
|
edges.push({ from: id, to: connId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, edges }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的力导向布局算法
|
||||||
|
* 将节点分布在合理的坐标上
|
||||||
|
* @param {Object} graph - 地图图形结构
|
||||||
|
* @param {Object} options - 布局选项
|
||||||
|
* @returns {Object} 带有坐标的节点
|
||||||
|
*/
|
||||||
|
export function layoutMap(graph, options = {}) {
|
||||||
|
const { nodes, edges } = graph
|
||||||
|
const { width = 300, height = 400 } = options
|
||||||
|
|
||||||
|
// 创建邻接表
|
||||||
|
const adj = {}
|
||||||
|
for (const node of nodes) {
|
||||||
|
adj[node.id] = []
|
||||||
|
}
|
||||||
|
for (const edge of edges) {
|
||||||
|
adj[edge.from].push(edge.to)
|
||||||
|
adj[edge.to].push(edge.from)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到中心节点(连接最多的节点作为起点)
|
||||||
|
let centerNode = nodes[0]
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (adj[node.id].length > adj[centerNode.id].length) {
|
||||||
|
centerNode = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS 分层布局
|
||||||
|
const levels = {}
|
||||||
|
const visited = new Set()
|
||||||
|
const queue = [{ id: centerNode.id, level: 0 }]
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { id, level } = queue.shift()
|
||||||
|
if (visited.has(id)) continue
|
||||||
|
visited.add(id)
|
||||||
|
|
||||||
|
if (!levels[level]) levels[level] = []
|
||||||
|
levels[level].push(id)
|
||||||
|
|
||||||
|
for (const neighbor of adj[id]) {
|
||||||
|
if (!visited.has(neighbor)) {
|
||||||
|
queue.push({ id: neighbor, level: level + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算节点位置
|
||||||
|
const nodePositions = {}
|
||||||
|
const levelHeight = height / (Object.keys(levels).length + 1)
|
||||||
|
|
||||||
|
for (const [level, nodeIds] of Object.entries(levels)) {
|
||||||
|
const y = (parseInt(level) + 1) * levelHeight
|
||||||
|
const levelWidth = width / (nodeIds.length + 1)
|
||||||
|
|
||||||
|
nodeIds.forEach((nodeId, index) => {
|
||||||
|
nodePositions[nodeId] = {
|
||||||
|
x: (index + 1) * levelWidth,
|
||||||
|
y
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新节点坐标
|
||||||
|
return nodes.map(node => ({
|
||||||
|
...node,
|
||||||
|
x: nodePositions[node.id]?.x || width / 2,
|
||||||
|
y: nodePositions[node.id]?.y || height / 2
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取从当前位置可达的区域(包括多步可达)
|
||||||
|
* @param {string} currentLocation - 当前位置ID
|
||||||
|
* @param {number} maxDistance - 最大距离(步数)
|
||||||
|
* @returns {Object} 可达区域信息,key为locationId,value为距离
|
||||||
|
*/
|
||||||
|
export function getReachableLocations(currentLocation, maxDistance = 10) {
|
||||||
|
const distances = {}
|
||||||
|
const queue = [{ id: currentLocation, dist: 0 }]
|
||||||
|
|
||||||
|
distances[currentLocation] = 0
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { id, dist } = queue.shift()
|
||||||
|
|
||||||
|
if (dist >= maxDistance) continue
|
||||||
|
|
||||||
|
// 找到所有连接的节点
|
||||||
|
const connections = LOCATION_CONFIG[id]?.connections || []
|
||||||
|
for (const connId of connections) {
|
||||||
|
if (distances[connId] === undefined) {
|
||||||
|
distances[connId] = dist + 1
|
||||||
|
queue.push({ id: connId, dist: dist + 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return distances
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图的路径信息
|
||||||
|
* @param {string} from - 起点
|
||||||
|
* @param {string} to - 终点
|
||||||
|
* @returns {Array} 路径数组
|
||||||
|
*/
|
||||||
|
export function getPath(from, to) {
|
||||||
|
if (from === to) return [from]
|
||||||
|
|
||||||
|
const { edges } = buildMapGraph()
|
||||||
|
const adj = {}
|
||||||
|
for (const node of Object.keys(LOCATION_CONFIG)) {
|
||||||
|
adj[node] = LOCATION_CONFIG[node]?.connections || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS 寻找最短路径
|
||||||
|
const queue = [[from]]
|
||||||
|
const visited = new Set([from])
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const path = queue.shift()
|
||||||
|
const current = path[path.length - 1]
|
||||||
|
|
||||||
|
if (current === to) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const neighbor of adj[current]) {
|
||||||
|
if (!visited.has(neighbor)) {
|
||||||
|
visited.add(neighbor)
|
||||||
|
queue.push([...path, neighbor])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null // 无路径
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预定义的地图布局配置
|
||||||
|
* 为每个区域指定固定的显示位置
|
||||||
|
*/
|
||||||
|
export const PREDEFINED_LAYOUT = {
|
||||||
|
// 营地为中心
|
||||||
|
camp: { x: 150, y: 200 },
|
||||||
|
// 营地周围
|
||||||
|
market: { x: 50, y: 120 },
|
||||||
|
blackmarket: { x: 250, y: 120 },
|
||||||
|
wild1: { x: 150, y: 320 },
|
||||||
|
// 更远的地方
|
||||||
|
boss_lair: { x: 150, y: 440 },
|
||||||
|
basement: { x: 150, y: 540 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用预定义布局获取节点位置
|
||||||
|
* @param {Object} graph - 地图图形结构
|
||||||
|
* @returns {Array} 带有坐标的节点
|
||||||
|
*/
|
||||||
|
export function getPredefinedLayout(graph) {
|
||||||
|
const { nodes } = graph
|
||||||
|
|
||||||
|
return nodes.map(node => ({
|
||||||
|
...node,
|
||||||
|
x: PREDEFINED_LAYOUT[node.id]?.x || 150,
|
||||||
|
y: PREDEFINED_LAYOUT[node.id]?.y || 200
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可视化的地图数据
|
||||||
|
* @param {string} currentLocation - 当前位置
|
||||||
|
* @returns {Object} 地图数据
|
||||||
|
*/
|
||||||
|
export function getMapData(currentLocation) {
|
||||||
|
const graph = buildMapGraph()
|
||||||
|
const layoutNodes = getPredefinedLayout(graph)
|
||||||
|
|
||||||
|
// 计算画布大小
|
||||||
|
let maxX = 0, maxY = 0
|
||||||
|
for (const node of layoutNodes) {
|
||||||
|
maxX = Math.max(maxX, node.x)
|
||||||
|
maxY = Math.max(maxY, node.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可达距离
|
||||||
|
const reachable = getReachableLocations(currentLocation, 10)
|
||||||
|
|
||||||
|
// 计算到每个区域的路径
|
||||||
|
const paths = {}
|
||||||
|
for (const node of layoutNodes) {
|
||||||
|
const path = getPath(currentLocation, node.id)
|
||||||
|
if (path) {
|
||||||
|
paths[node.id] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: layoutNodes,
|
||||||
|
edges: graph.edges,
|
||||||
|
width: maxX + 50,
|
||||||
|
height: maxY + 50,
|
||||||
|
currentLocation,
|
||||||
|
reachable,
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 义体系统 - 安装、卸下、义体适应性
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装义体
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} prostheticItem - 义体物品
|
||||||
|
* @returns {Object} { success: boolean, message: string }
|
||||||
|
*/
|
||||||
|
export function installProsthetic(playerStore, gameStore, prostheticItem) {
|
||||||
|
// 检查是否已有义体
|
||||||
|
if (playerStore.equipment.prosthetic) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '你已经装备了义体,需要先卸下当前的义体。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查义体类型
|
||||||
|
if (prostheticItem.type !== 'prosthetic') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '这不是义体装备。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 装备义体
|
||||||
|
playerStore.equipment.prosthetic = { ...prostheticItem }
|
||||||
|
|
||||||
|
// 解锁义体适应性技能
|
||||||
|
if (!playerStore.skills['prosthetic_adaptation']) {
|
||||||
|
playerStore.skills['prosthetic_adaptation'] = { level: 0, exp: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.addLog(`安装了 ${prostheticItem.name}`, 'reward')
|
||||||
|
gameStore.addLog(`获得了被动技能: 义体适应性`, 'info')
|
||||||
|
|
||||||
|
// 检查成就
|
||||||
|
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `成功安装 ${prostheticItem.name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸下义体
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Object} { success: boolean, message: string, removedItem: Object }
|
||||||
|
*/
|
||||||
|
export function removeProsthetic(playerStore, gameStore) {
|
||||||
|
const currentProsthetic = playerStore.equipment.prosthetic
|
||||||
|
|
||||||
|
if (!currentProsthetic) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '你没有装备任何义体。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将义体放回背包
|
||||||
|
playerStore.inventory.push({ ...currentProsthetic })
|
||||||
|
|
||||||
|
// 卸下义体
|
||||||
|
playerStore.equipment.prosthetic = null
|
||||||
|
|
||||||
|
gameStore.addLog(`卸下了 ${currentProsthetic.name}`, 'info')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `成功卸下 ${currentProsthetic.name}`,
|
||||||
|
removedItem: currentProsthetic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取义体提供的属性加成
|
||||||
|
* @param {Object} prosthetic - 义体装备
|
||||||
|
* @returns {Object} 属性加成
|
||||||
|
*/
|
||||||
|
export function getProstheticBonus(prosthetic) {
|
||||||
|
if (!prosthetic) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: prosthetic.stats || {},
|
||||||
|
skill: prosthetic.skill || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查义体技能是否可用
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} skillId - 技能ID
|
||||||
|
* @returns {Boolean} 是否可用
|
||||||
|
*/
|
||||||
|
export function isProstheticSkillAvailable(playerStore, skillId) {
|
||||||
|
const prosthetic = playerStore.equipment.prosthetic
|
||||||
|
if (!prosthetic) return false
|
||||||
|
|
||||||
|
// 检查义体是否提供该技能
|
||||||
|
return prosthetic.skill === skillId || prosthetic.grantedSkills?.includes(skillId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加义体适应性经验
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Number} exp - 经验值
|
||||||
|
*/
|
||||||
|
export function addProstheticExp(playerStore, gameStore, exp) {
|
||||||
|
const skill = playerStore.skills['prosthetic_adaptation']
|
||||||
|
if (!skill) return
|
||||||
|
|
||||||
|
const { SKILL_CONFIG } = require('@/config/skills.js')
|
||||||
|
const skillConfig = SKILL_CONFIG['prosthetic_adaptation']
|
||||||
|
const maxLevel = skillConfig.maxLevel
|
||||||
|
|
||||||
|
if (skill.level >= maxLevel) return
|
||||||
|
|
||||||
|
skill.exp += exp
|
||||||
|
const expNeeded = skillConfig.expPerLevel(skill.level)
|
||||||
|
|
||||||
|
while (skill.exp >= expNeeded && skill.level < maxLevel) {
|
||||||
|
skill.exp -= expNeeded
|
||||||
|
skill.level++
|
||||||
|
gameStore.addLog(`义体适应性提升到 Lv.${skill.level}!`, 'reward')
|
||||||
|
|
||||||
|
// 检查里程碑奖励
|
||||||
|
if (skillConfig.milestones[skill.level]) {
|
||||||
|
const milestone = skillConfig.milestones[skill.level]
|
||||||
|
gameStore.addLog(`解锁效果: ${milestone.desc}`, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skill.level < maxLevel) {
|
||||||
|
expNeeded = skillConfig.expPerLevel(skill.level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,96 +265,6 @@ export function unlockSkill(playerStore, skillId) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查技能是否可解锁
|
|
||||||
* @param {Object} playerStore - 玩家Store
|
|
||||||
* @param {String} skillId - 技能ID
|
|
||||||
* @returns {Object} { canUnlock: boolean, reason: string }
|
|
||||||
*/
|
|
||||||
export function canUnlockSkill(playerStore, skillId) {
|
|
||||||
const config = SKILL_CONFIG[skillId]
|
|
||||||
if (!config) {
|
|
||||||
return { canUnlock: false, reason: '技能不存在' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已解锁
|
|
||||||
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
|
|
||||||
return { canUnlock: true, reason: '已解锁' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查解锁条件
|
|
||||||
if (config.unlockCondition) {
|
|
||||||
const condition = config.unlockCondition
|
|
||||||
|
|
||||||
// 物品条件
|
|
||||||
if (condition.type === 'item') {
|
|
||||||
const hasItem = playerStore.inventory?.some(i => i.id === condition.item)
|
|
||||||
if (!hasItem) {
|
|
||||||
return { canUnlock: false, reason: '需要特定物品' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 位置条件
|
|
||||||
if (condition.location) {
|
|
||||||
if (playerStore.currentLocation !== condition.location) {
|
|
||||||
return { canUnlock: false, reason: '需要在特定位置解锁' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 技能等级条件
|
|
||||||
if (condition.skillLevel) {
|
|
||||||
const requiredSkill = playerStore.skills[condition.skillId]
|
|
||||||
if (!requiredSkill || requiredSkill.level < condition.skillLevel) {
|
|
||||||
return { canUnlock: false, reason: '需要前置技能达到特定等级' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { canUnlock: true, reason: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取技能经验倍率(含父技能加成)
|
|
||||||
* @param {Object} playerStore - 玩家Store
|
|
||||||
* @param {String} skillId - 技能ID
|
|
||||||
* @returns {Number} 经验倍率
|
|
||||||
*/
|
|
||||||
export function getSkillExpRate(playerStore, skillId) {
|
|
||||||
let rate = 1.0
|
|
||||||
|
|
||||||
// 全局经验加成
|
|
||||||
if (playerStore.globalBonus && playerStore.globalBonus.globalExpRate) {
|
|
||||||
rate *= (1 + playerStore.globalBonus.globalExpRate / 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 父技能加成
|
|
||||||
const config = SKILL_CONFIG[skillId]
|
|
||||||
if (config && config.parentSkill) {
|
|
||||||
const parentSkill = playerStore.skills[config.parentSkill]
|
|
||||||
if (parentSkill && parentSkill.unlocked) {
|
|
||||||
const parentConfig = SKILL_CONFIG[config.parentSkill]
|
|
||||||
if (parentConfig && parentConfig.milestones) {
|
|
||||||
// 检查父技能里程碑奖励
|
|
||||||
for (const [level, milestone] of Object.entries(parentConfig.milestones)) {
|
|
||||||
if (parentSkill.level >= parseInt(level) && milestone.effect?.expRate) {
|
|
||||||
rate *= milestone.effect.expRate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果子技能等级低于父技能,提供额外加成
|
|
||||||
if (parentSkill.level > 0 && playerStore.skills[skillId]) {
|
|
||||||
const childLevel = playerStore.skills[skillId].level
|
|
||||||
if (childLevel < parentSkill.level) {
|
|
||||||
rate *= 1.5 // 父技能倍率加成
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rate
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取技能显示信息
|
* 获取技能显示信息
|
||||||
* @param {Object} playerStore - 玩家Store
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* 音效系统
|
||||||
|
* Phase 3 UI美化 - 音效支持
|
||||||
|
*
|
||||||
|
* 注意:uni-app的音频功能有限,不同平台支持情况不同
|
||||||
|
* H5端:使用Web Audio API
|
||||||
|
* 小程序端:使用 wx.createInnerAudioContext()
|
||||||
|
* App端:使用 plus.audio
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音效类型枚举
|
||||||
|
*/
|
||||||
|
export const SOUND_TYPES = {
|
||||||
|
// UI音效
|
||||||
|
CLICK: 'click',
|
||||||
|
OPEN: 'open',
|
||||||
|
CLOSE: 'close',
|
||||||
|
TOGGLE: 'toggle',
|
||||||
|
|
||||||
|
// 游戏音效
|
||||||
|
COMBAT_START: 'combat_start',
|
||||||
|
COMBAT_HIT: 'combat_hit',
|
||||||
|
COMBAT_HIT_PLAYER: 'combat_hit_player',
|
||||||
|
COMBAT_VICTORY: 'combat_victory',
|
||||||
|
COMBAT_DEFEAT: 'combat_defeat',
|
||||||
|
|
||||||
|
// 奖励音效
|
||||||
|
REWARD: 'reward',
|
||||||
|
LEVEL_UP: 'level_up',
|
||||||
|
SKILL_UP: 'skill_up',
|
||||||
|
|
||||||
|
// 错误音效
|
||||||
|
ERROR: 'error',
|
||||||
|
WARNING: 'warning',
|
||||||
|
|
||||||
|
// 系统音效
|
||||||
|
NOTIFICATION: 'notification',
|
||||||
|
MESSAGE: 'message'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音效配置
|
||||||
|
*/
|
||||||
|
const SOUND_CONFIG = {
|
||||||
|
// 是否启用音效
|
||||||
|
enabled: true,
|
||||||
|
// 音量 (0-1)
|
||||||
|
volume: 0.5,
|
||||||
|
// 音效文件路径配置 (需要实际文件)
|
||||||
|
soundPaths: {
|
||||||
|
[SOUND_TYPES.CLICK]: '/static/sounds/click.mp3',
|
||||||
|
[SOUND_TYPES.OPEN]: '/static/sounds/open.mp3',
|
||||||
|
[SOUND_TYPES.CLOSE]: '/static/sounds/close.mp3',
|
||||||
|
[SOUND_TYPES.TOGGLE]: '/static/sounds/toggle.mp3',
|
||||||
|
[SOUND_TYPES.COMBAT_START]: '/static/sounds/combat_start.mp3',
|
||||||
|
[SOUND_TYPES.COMBAT_HIT]: '/static/sounds/combat_hit.mp3',
|
||||||
|
[SOUND_TYPES.COMBAT_HIT_PLAYER]: '/static/sounds/combat_hit_player.mp3',
|
||||||
|
[SOUND_TYPES.COMBAT_VICTORY]: '/static/sounds/combat_victory.mp3',
|
||||||
|
[SOUND_TYPES.COMBAT_DEFEAT]: '/static/sounds/combat_defeat.mp3',
|
||||||
|
[SOUND_TYPES.REWARD]: '/static/sounds/reward.mp3',
|
||||||
|
[SOUND_TYPES.LEVEL_UP]: '/static/sounds/level_up.mp3',
|
||||||
|
[SOUND_TYPES.SKILL_UP]: '/static/sounds/skill_up.mp3',
|
||||||
|
[SOUND_TYPES.ERROR]: '/static/sounds/error.mp3',
|
||||||
|
[SOUND_TYPES.WARNING]: '/static/sounds/warning.mp3',
|
||||||
|
[SOUND_TYPES.NOTIFICATION]: '/static/sounds/notification.mp3',
|
||||||
|
[SOUND_TYPES.MESSAGE]: '/static/sounds/message.mp3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频上下文缓存
|
||||||
|
*/
|
||||||
|
let audioContext = null
|
||||||
|
let audioCache = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化音频系统
|
||||||
|
*/
|
||||||
|
export function initSoundSystem() {
|
||||||
|
// #ifdef H5
|
||||||
|
try {
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)()
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Web Audio API not supported:', e)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 小程序端使用微信音频API
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 从本地存储加载音效设置
|
||||||
|
loadSoundSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放音效
|
||||||
|
* @param {String} soundType - 音效类型
|
||||||
|
* @param {Object} options - 选项 { volume, speed }
|
||||||
|
*/
|
||||||
|
export function playSound(soundType, options = {}) {
|
||||||
|
if (!SOUND_CONFIG.enabled) {
|
||||||
|
return { success: false, message: '音效已禁用' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = options.volume !== undefined ? options.volume : SOUND_CONFIG.volume
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
return playSoundH5(soundType, volume)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return playSoundWeixin(soundType, volume)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
return playSoundApp(soundType, volume)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return { success: false, message: '当前平台不支持音效' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5端播放音效 (使用Web Audio API合成简单音效)
|
||||||
|
*/
|
||||||
|
function playSoundH5(soundType, volume) {
|
||||||
|
if (!audioContext) {
|
||||||
|
initSoundSystem()
|
||||||
|
if (!audioContext) {
|
||||||
|
return { success: false, message: '音频上下文初始化失败' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复音频上下文(某些浏览器需要用户交互后才能恢复)
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
audioContext.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建振荡器生成简单音效
|
||||||
|
const oscillator = audioContext.createOscillator()
|
||||||
|
const gainNode = audioContext.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
|
// 根据音效类型设置不同的频率和包络
|
||||||
|
const soundParams = getSoundParameters(soundType)
|
||||||
|
oscillator.type = soundParams.type || 'sine'
|
||||||
|
oscillator.frequency.setValueAtTime(soundParams.frequency, audioContext.currentTime)
|
||||||
|
|
||||||
|
// 设置音量包络
|
||||||
|
const now = audioContext.currentTime
|
||||||
|
gainNode.gain.setValueAtTime(0, now)
|
||||||
|
gainNode.gain.linearRampToValueAtTime(volume * 0.3, now + 0.01)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.001, now + soundParams.duration)
|
||||||
|
|
||||||
|
oscillator.start(now)
|
||||||
|
oscillator.stop(now + soundParams.duration)
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('播放音效失败:', e)
|
||||||
|
return { success: false, message: e.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音效参数 (用于合成音效)
|
||||||
|
*/
|
||||||
|
function getSoundParameters(soundType) {
|
||||||
|
const params = {
|
||||||
|
// 点击音效 - 短促的高音
|
||||||
|
[SOUND_TYPES.CLICK]: { type: 'sine', frequency: 800, duration: 0.05 },
|
||||||
|
// 打开音效 - 上升音调
|
||||||
|
[SOUND_TYPES.OPEN]: { type: 'sine', frequency: 400, duration: 0.15 },
|
||||||
|
// 关闭音效 - 下降音调
|
||||||
|
[SOUND_TYPES.CLOSE]: { type: 'sine', frequency: 300, duration: 0.1 },
|
||||||
|
// 切换音效
|
||||||
|
[SOUND_TYPES.TOGGLE]: { type: 'square', frequency: 500, duration: 0.08 },
|
||||||
|
// 战斗开始 - 低沉
|
||||||
|
[SOUND_TYPES.COMBAT_START]: { type: 'sawtooth', frequency: 150, duration: 0.3 },
|
||||||
|
// 命中敌人 - 尖锐
|
||||||
|
[SOUND_TYPES.COMBAT_HIT]: { type: 'square', frequency: 1200, duration: 0.08 },
|
||||||
|
// 被命中 - 低频
|
||||||
|
[SOUND_TYPES.COMBAT_HIT_PLAYER]: { type: 'sawtooth', frequency: 200, duration: 0.15 },
|
||||||
|
// 胜利 - 上升音阶
|
||||||
|
[SOUND_TYPES.COMBAT_VICTORY]: { type: 'sine', frequency: 523, duration: 0.4 },
|
||||||
|
// 失败 - 下降音
|
||||||
|
[SOUND_TYPES.COMBAT_DEFEAT]: { type: 'sawtooth', frequency: 100, duration: 0.4 },
|
||||||
|
// 奖励 - 悦耳的高音
|
||||||
|
[SOUND_TYPES.REWARD]: { type: 'sine', frequency: 880, duration: 0.2 },
|
||||||
|
// 升级 - 双音
|
||||||
|
[SOUND_TYPES.LEVEL_UP]: { type: 'sine', frequency: 659, duration: 0.3 },
|
||||||
|
// 技能升级
|
||||||
|
[SOUND_TYPES.SKILL_UP]: { type: 'sine', frequency: 784, duration: 0.25 },
|
||||||
|
// 错误 - 低频嗡鸣
|
||||||
|
[SOUND_TYPES.ERROR]: { type: 'sawtooth', frequency: 100, duration: 0.2 },
|
||||||
|
// 警告
|
||||||
|
[SOUND_TYPES.WARNING]: { type: 'square', frequency: 440, duration: 0.15 },
|
||||||
|
// 通知
|
||||||
|
[SOUND_TYPES.NOTIFICATION]: { type: 'sine', frequency: 660, duration: 0.2 },
|
||||||
|
// 消息
|
||||||
|
[SOUND_TYPES.MESSAGE]: { type: 'sine', frequency: 587, duration: 0.15 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return params[soundType] || { type: 'sine', frequency: 440, duration: 0.1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序端播放音效
|
||||||
|
*/
|
||||||
|
function playSoundWeixin(soundType, volume) {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const soundPath = SOUND_CONFIG.soundPaths[soundType]
|
||||||
|
if (!soundPath) {
|
||||||
|
return { success: false, message: '音效文件未配置' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const audio = uni.createInnerAudioContext()
|
||||||
|
audio.src = soundPath
|
||||||
|
audio.volume = volume
|
||||||
|
audio.play()
|
||||||
|
return { success: true }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, message: e.message }
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return { success: false, message: '非微信小程序环境' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App端播放音效
|
||||||
|
*/
|
||||||
|
function playSoundApp(soundType, volume) {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
const soundPath = SOUND_CONFIG.soundPaths[soundType]
|
||||||
|
if (!soundPath) {
|
||||||
|
return { success: false, message: '音效文件未配置' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const player = plus.audio.createPlayer(soundPath)
|
||||||
|
player.setVolume(volume * 100)
|
||||||
|
player.play()
|
||||||
|
return { success: true }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, message: e.message }
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return { success: false, message: '非App环境' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置音效开关
|
||||||
|
* @param {Boolean} enabled - 是否启用音效
|
||||||
|
*/
|
||||||
|
export function setSoundEnabled(enabled) {
|
||||||
|
SOUND_CONFIG.enabled = enabled
|
||||||
|
saveSoundSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置音量
|
||||||
|
* @param {Number} volume - 音量 (0-1)
|
||||||
|
*/
|
||||||
|
export function setSoundVolume(volume) {
|
||||||
|
SOUND_CONFIG.volume = Math.max(0, Math.min(1, volume))
|
||||||
|
saveSoundSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音效设置
|
||||||
|
* @returns {Object} { enabled, volume }
|
||||||
|
*/
|
||||||
|
export function getSoundSettings() {
|
||||||
|
return {
|
||||||
|
enabled: SOUND_CONFIG.enabled,
|
||||||
|
volume: SOUND_CONFIG.volume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存音效设置到本地存储
|
||||||
|
*/
|
||||||
|
function saveSoundSettings() {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync('soundSettings', JSON.stringify({
|
||||||
|
enabled: SOUND_CONFIG.enabled,
|
||||||
|
volume: SOUND_CONFIG.volume
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('保存音效设置失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地存储加载音效设置
|
||||||
|
*/
|
||||||
|
function loadSoundSettings() {
|
||||||
|
try {
|
||||||
|
const settings = uni.getStorageSync('soundSettings')
|
||||||
|
if (settings) {
|
||||||
|
const parsed = JSON.parse(settings)
|
||||||
|
SOUND_CONFIG.enabled = parsed.enabled !== undefined ? parsed.enabled : true
|
||||||
|
SOUND_CONFIG.volume = parsed.volume !== undefined ? parsed.volume : 0.5
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('加载音效设置失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷音效播放函数
|
||||||
|
*/
|
||||||
|
export const playClick = () => playSound(SOUND_TYPES.CLICK)
|
||||||
|
export const playOpen = () => playSound(SOUND_TYPES.OPEN)
|
||||||
|
export const playClose = () => playSound(SOUND_TYPES.CLOSE)
|
||||||
|
export const playCombatStart = () => playSound(SOUND_TYPES.COMBAT_START)
|
||||||
|
export const playCombatHit = () => playSound(SOUND_TYPES.COMBAT_HIT)
|
||||||
|
export const playCombatVictory = () => playSound(SOUND_TYPES.COMBAT_VICTORY)
|
||||||
|
export const playCombatDefeat = () => playSound(SOUND_TYPES.COMBAT_DEFEAT)
|
||||||
|
export const playReward = () => playSound(SOUND_TYPES.REWARD)
|
||||||
|
export const playLevelUp = () => playSound(SOUND_TYPES.LEVEL_UP)
|
||||||
|
export const playSkillUp = () => playSound(SOUND_TYPES.SKILL_UP)
|
||||||
|
export const playError = () => playSound(SOUND_TYPES.ERROR)
|
||||||
|
export const playWarning = () => playSound(SOUND_TYPES.WARNING)
|
||||||
|
|
||||||
|
// 初始化音效系统
|
||||||
|
initSoundSystem()
|
||||||
@@ -155,6 +155,7 @@ function serializeGameData(gameStore) {
|
|||||||
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
|
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
|
||||||
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
|
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
|
||||||
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
|
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
|
||||||
|
autoCombat: gameStore.autoCombat || false, // 保存自动战斗状态
|
||||||
// 不保存日志(logs不持久化)
|
// 不保存日志(logs不持久化)
|
||||||
// 不保存战斗状态(重新登录时退出战斗)
|
// 不保存战斗状态(重新登录时退出战斗)
|
||||||
// 不保存当前事件(重新登录时清除)
|
// 不保存当前事件(重新登录时清除)
|
||||||
@@ -262,6 +263,11 @@ function applyGameData(gameStore, data) {
|
|||||||
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
|
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动战斗状态
|
||||||
|
if (data.autoCombat !== undefined) {
|
||||||
|
gameStore.autoCombat = data.autoCombat
|
||||||
|
}
|
||||||
|
|
||||||
// 定时事件
|
// 定时事件
|
||||||
if (data.scheduledEvents) {
|
if (data.scheduledEvents) {
|
||||||
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
|
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
|
||||||
|
|||||||
+191
-15
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { SKILL_CONFIG } from '@/config/skills.js'
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
import { ITEM_CONFIG } from '@/config/items.js'
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
|
import { RECIPE_CONFIG } from '@/config/recipes.js'
|
||||||
|
import { addSkillExp, unlockSkill } from './skillSystem.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务类型枚举
|
* 任务类型枚举
|
||||||
@@ -16,7 +18,8 @@ export const TASK_TYPES = {
|
|||||||
WORKING: 'working', // 工作
|
WORKING: 'working', // 工作
|
||||||
COMBAT: 'combat', // 战斗
|
COMBAT: 'combat', // 战斗
|
||||||
EXPLORE: 'explore', // 探索
|
EXPLORE: 'explore', // 探索
|
||||||
PRAYING: 'praying' // 祈祷
|
PRAYING: 'praying', // 祈祷
|
||||||
|
CRAFTING: 'crafting' // 制造
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,15 +30,19 @@ const MUTEX_RULES = {
|
|||||||
full: [
|
full: [
|
||||||
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
|
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
|
||||||
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
|
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
|
||||||
|
[TASK_TYPES.COMBAT, TASK_TYPES.CRAFTING], // 战斗 vs 制造
|
||||||
[TASK_TYPES.TRAINING, TASK_TYPES.WORKING], // 训练 vs 工作
|
[TASK_TYPES.TRAINING, TASK_TYPES.WORKING], // 训练 vs 工作
|
||||||
[TASK_TYPES.EXPLORE, TASK_TYPES.RESTING] // 探索 vs 休息
|
[TASK_TYPES.TRAINING, TASK_TYPES.CRAFTING], // 训练 vs 制造
|
||||||
|
[TASK_TYPES.EXPLORE, TASK_TYPES.RESTING], // 探索 vs 休息
|
||||||
|
[TASK_TYPES.EXPLORE, TASK_TYPES.CRAFTING] // 探索 vs 制造
|
||||||
],
|
],
|
||||||
|
|
||||||
// 部分互斥(需要一心多用技能)
|
// 部分互斥(需要一心多用技能)
|
||||||
partial: [
|
partial: [
|
||||||
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
|
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
|
||||||
[TASK_TYPES.READING, TASK_TYPES.TRAINING], // 阅读 vs 训练
|
[TASK_TYPES.READING, TASK_TYPES.TRAINING], // 阅读 vs 训练
|
||||||
[TASK_TYPES.READING, TASK_TYPES.WORKING] // 阅读 vs 工作
|
[TASK_TYPES.READING, TASK_TYPES.WORKING], // 阅读 vs 工作
|
||||||
|
[TASK_TYPES.READING, TASK_TYPES.CRAFTING] // 阅读 vs 制造
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +189,17 @@ function checkTaskConditions(playerStore, taskType, taskData) {
|
|||||||
return { canStart: false, reason: '需要圣经' }
|
return { canStart: false, reason: '需要圣经' }
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case TASK_TYPES.CRAFTING:
|
||||||
|
// 需要配方ID
|
||||||
|
if (!taskData.recipeId) {
|
||||||
|
return { canStart: false, reason: '需要选择配方' }
|
||||||
|
}
|
||||||
|
// 需要耐力
|
||||||
|
if (playerStore.currentStats.stamina < 5) {
|
||||||
|
return { canStart: false, reason: '耐力不足' }
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return { canStart: true, reason: '' }
|
return { canStart: true, reason: '' }
|
||||||
@@ -275,6 +293,9 @@ function processSingleTask(gameStore, playerStore, task, deltaTime) {
|
|||||||
case TASK_TYPES.PRAYING:
|
case TASK_TYPES.PRAYING:
|
||||||
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
|
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
|
||||||
|
|
||||||
|
case TASK_TYPES.CRAFTING:
|
||||||
|
return processCraftingTask(gameStore, playerStore, task, elapsedSeconds)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { completed: false }
|
return { completed: false }
|
||||||
}
|
}
|
||||||
@@ -291,6 +312,9 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
const readingTime = bookConfig.readingTime || 60
|
const readingTime = bookConfig.readingTime || 60
|
||||||
|
|
||||||
|
// 首次阅读时解锁阅读技能
|
||||||
|
unlockSkill(playerStore, 'reading')
|
||||||
|
|
||||||
// 检查环境惩罚
|
// 检查环境惩罚
|
||||||
const location = playerStore.currentLocation
|
const location = playerStore.currentLocation
|
||||||
let timeMultiplier = 1.0
|
let timeMultiplier = 1.0
|
||||||
@@ -301,9 +325,26 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
timeMultiplier = 0.5 + (darkPenaltyReduce / 100) * 0.5
|
timeMultiplier = 0.5 + (darkPenaltyReduce / 100) * 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 阅读技能速度加成
|
||||||
|
const readingSkillLevel = playerStore.skills.reading?.level || 0
|
||||||
|
const readingSpeedBonus = playerStore.globalBonus?.readingSpeed || 1
|
||||||
|
timeMultiplier *= readingSpeedBonus
|
||||||
|
|
||||||
// 计算有效进度
|
// 计算有效进度
|
||||||
const effectiveProgress = task.progress * timeMultiplier
|
const effectiveProgress = task.progress * timeMultiplier
|
||||||
|
|
||||||
|
// 给予阅读技能经验(每秒给予经验)
|
||||||
|
const readingExpPerSecond = 1
|
||||||
|
const readingExpGain = readingExpPerSecond * elapsedSeconds * timeMultiplier
|
||||||
|
const readingResult = addSkillExp(playerStore, 'reading', readingExpGain)
|
||||||
|
|
||||||
|
// 检查技能是否升级
|
||||||
|
if (readingResult.leveledUp) {
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`阅读技能升级到了 Lv.${readingResult.newLevel}!`, 'reward')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (effectiveProgress >= readingTime) {
|
if (effectiveProgress >= readingTime) {
|
||||||
// 阅读完成
|
// 阅读完成
|
||||||
const rewards = {}
|
const rewards = {}
|
||||||
@@ -322,13 +363,13 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
// 添加日志
|
// 添加日志
|
||||||
if (gameStore.addLog) {
|
if (gameStore.addLog) {
|
||||||
gameStore.addLog(`读完了《${bookConfig.name}》`, 'reward')
|
gameStore.addLog(`读完了《${bookConfig.name}》,阅读经验+${Math.floor(readingExpGain * task.progress / readingTime)}`, 'reward')
|
||||||
}
|
}
|
||||||
|
|
||||||
return { completed: true, rewards }
|
return { completed: true, rewards }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阅读进行中,给予少量经验
|
// 阅读进行中,给予其他技能经验
|
||||||
if (bookConfig.expReward) {
|
if (bookConfig.expReward) {
|
||||||
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
|
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
|
||||||
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
|
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
|
||||||
@@ -391,8 +432,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
return { completed: true, error: '技能不存在' }
|
return { completed: true, error: '技能不存在' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消耗耐力
|
// 消耗耐力(降低消耗)
|
||||||
const staminaCost = 2 * elapsedSeconds
|
const staminaCost = 1 * elapsedSeconds
|
||||||
if (playerStore.currentStats.stamina < staminaCost) {
|
if (playerStore.currentStats.stamina < staminaCost) {
|
||||||
// 耐力不足,任务结束
|
// 耐力不足,任务结束
|
||||||
return { completed: true, rewards: {} }
|
return { completed: true, rewards: {} }
|
||||||
@@ -400,8 +441,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
playerStore.currentStats.stamina -= staminaCost
|
playerStore.currentStats.stamina -= staminaCost
|
||||||
|
|
||||||
// 给予技能经验
|
// 给予技能经验(提升获取速度)
|
||||||
const expPerSecond = 1
|
const expPerSecond = 5 // 从1提升到5
|
||||||
const expGain = expPerSecond * elapsedSeconds
|
const expGain = expPerSecond * elapsedSeconds
|
||||||
|
|
||||||
if (!task.accumulatedExp) {
|
if (!task.accumulatedExp) {
|
||||||
@@ -412,6 +453,12 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
}
|
}
|
||||||
task.accumulatedExp[skillId] += expGain
|
task.accumulatedExp[skillId] += expGain
|
||||||
|
|
||||||
|
// 检查技能是否升级
|
||||||
|
const result = addSkillExp(playerStore, skillId, expGain)
|
||||||
|
if (result.leveledUp && gameStore.addLog) {
|
||||||
|
gameStore.addLog(`${skillConfig.name}升级到了 Lv.${result.newLevel}!`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否完成
|
// 检查是否完成
|
||||||
if (task.data.duration && task.progress >= task.data.duration) {
|
if (task.data.duration && task.progress >= task.data.duration) {
|
||||||
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }
|
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }
|
||||||
@@ -491,6 +538,134 @@ function processPrayingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
return { completed: false }
|
return { completed: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理制造任务
|
||||||
|
*/
|
||||||
|
function processCraftingTask(gameStore, playerStore, task, elapsedSeconds) {
|
||||||
|
const recipeId = task.data.recipeId
|
||||||
|
const recipe = RECIPE_CONFIG[recipeId]
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
return { completed: true, error: '配方不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消耗耐力(制造也需要体力)
|
||||||
|
const staminaCost = 1 * elapsedSeconds
|
||||||
|
if (playerStore.currentStats.stamina < staminaCost) {
|
||||||
|
return { completed: true, rewards: {}, failed: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
playerStore.currentStats.stamina -= staminaCost
|
||||||
|
|
||||||
|
// 计算实际制造时间(受技能加成影响)
|
||||||
|
const craftingSkill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
let timeMultiplier = 1.0
|
||||||
|
|
||||||
|
if (craftingSkill && craftingSkill.level > 0) {
|
||||||
|
// 制造技能每级减少5%时间
|
||||||
|
timeMultiplier = 1 - (Math.min(craftingSkill.level, 20) * 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 累积制造进度(考虑时间加成)
|
||||||
|
const effectiveProgress = task.progress * timeMultiplier
|
||||||
|
|
||||||
|
// 检查是否完成
|
||||||
|
if (effectiveProgress >= recipe.baseTime) {
|
||||||
|
// 计算成功率
|
||||||
|
const successRate = calculateCraftingSuccessRate(playerStore, recipe)
|
||||||
|
|
||||||
|
// 判定是否成功
|
||||||
|
const success = Math.random() < successRate
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 给予制造技能经验
|
||||||
|
const expGain = recipe.baseTime * 0.5 // 基于制造时间的经验
|
||||||
|
|
||||||
|
if (!task.accumulatedExp) {
|
||||||
|
task.accumulatedExp = {}
|
||||||
|
}
|
||||||
|
if (!task.accumulatedExp[recipe.requiredSkill]) {
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] = 0
|
||||||
|
}
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] += expGain
|
||||||
|
|
||||||
|
const rewards = {
|
||||||
|
[recipe.requiredSkill]: task.accumulatedExp[recipe.requiredSkill] || 0,
|
||||||
|
craftedItem: recipe.resultItem,
|
||||||
|
craftedCount: recipe.resultCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加制造物品到背包(这将在任务完成回调中处理)
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
const itemConfig = ITEM_CONFIG[recipe.resultItem]
|
||||||
|
gameStore.addLog(`制造成功:${itemConfig?.name || recipe.resultItem}`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { completed: true, rewards, success: true }
|
||||||
|
} else {
|
||||||
|
// 制造失败
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`制造失败:${recipeId}`, 'warning')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败也给予少量经验
|
||||||
|
const expGain = recipe.baseTime * 0.1
|
||||||
|
if (!task.accumulatedExp) {
|
||||||
|
task.accumulatedExp = {}
|
||||||
|
}
|
||||||
|
if (!task.accumulatedExp[recipe.requiredSkill]) {
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] = 0
|
||||||
|
}
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] += expGain
|
||||||
|
|
||||||
|
const rewards = {
|
||||||
|
[recipe.requiredSkill]: task.accumulatedExp[recipe.requiredSkill] || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return { completed: true, rewards, success: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行中,给予少量经验
|
||||||
|
const expPerTick = 0.5 * elapsedSeconds
|
||||||
|
if (!task.accumulatedExp) {
|
||||||
|
task.accumulatedExp = {}
|
||||||
|
}
|
||||||
|
if (!task.accumulatedExp[recipe.requiredSkill]) {
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] = 0
|
||||||
|
}
|
||||||
|
task.accumulatedExp[recipe.requiredSkill] += expPerTick
|
||||||
|
|
||||||
|
return { completed: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算制造成功率
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 成功率 (0-1)
|
||||||
|
*/
|
||||||
|
function calculateCraftingSuccessRate(playerStore, recipe) {
|
||||||
|
let successRate = recipe.baseSuccessRate
|
||||||
|
|
||||||
|
// 技能等级加成(每级+2%)
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (skill && skill.level > 0) {
|
||||||
|
successRate += Math.min(skill.level, 20) * 0.02
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运气加成
|
||||||
|
const luck = playerStore.baseStats?.luck || 10
|
||||||
|
successRate += luck * 0.001
|
||||||
|
|
||||||
|
// 检查是否有制造技能的全局加成
|
||||||
|
if (playerStore.globalBonus?.craftingSuccessRate) {
|
||||||
|
successRate += playerStore.globalBonus.craftingSuccessRate / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(0.98, Math.max(0.05, successRate))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取任务奖励
|
* 获取任务奖励
|
||||||
*/
|
*/
|
||||||
@@ -521,17 +696,17 @@ function applyTaskRewards(playerStore, task, rewards) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (skillId === 'faith') {
|
if (skillId === 'faith') {
|
||||||
// 信仰技能经验
|
// 信仰技能经验 - 使用addSkillExp处理
|
||||||
if (!playerStore.skills.faith) {
|
if (!playerStore.skills.faith) {
|
||||||
playerStore.skills.faith = { level: 0, exp: 0, unlocked: true }
|
unlockSkill(playerStore, 'faith')
|
||||||
}
|
}
|
||||||
playerStore.skills.faith.exp += exp
|
addSkillExp(playerStore, 'faith', exp)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通技能经验
|
// 普通技能经验 - 使用addSkillExp处理升级和里程碑
|
||||||
if (playerStore.skills[skillId]) {
|
if (playerStore.skills[skillId]) {
|
||||||
playerStore.skills[skillId].exp += exp
|
addSkillExp(playerStore, skillId, exp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,7 +722,8 @@ function getTaskTypeName(taskType) {
|
|||||||
working: '工作',
|
working: '工作',
|
||||||
combat: '战斗',
|
combat: '战斗',
|
||||||
explore: '探索',
|
explore: '探索',
|
||||||
praying: '祈祷'
|
praying: '祈祷',
|
||||||
|
crafting: '制造'
|
||||||
}
|
}
|
||||||
return names[taskType] || taskType
|
return names[taskType] || taskType
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user