3101 lines
72 KiB
Markdown
3101 lines
72 KiB
Markdown
|
|
# 开发步骤文档
|
|||
|
|
|
|||
|
|
> 基于设计文档 v2.0 生成的完整开发指南
|
|||
|
|
> 项目:文字冒险游戏 (uni-app + Vue 3 + Pinia)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 文档说明
|
|||
|
|
|
|||
|
|
本文档将设计文档转化为可执行的开发步骤,每个Phase都包含:
|
|||
|
|
- **目标描述**:明确本阶段要实现的功能
|
|||
|
|
- **前置依赖**:需要先完成的条件
|
|||
|
|
- **详细任务清单**:包含具体文件路径和代码要点
|
|||
|
|
- **验收标准**:可测试的功能点
|
|||
|
|
- **预估文件列表**:本阶段将创建/修改的文件
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 1: 基础框架搭建
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
建立项目目录结构,安装核心依赖(Pinia),配置全局样式变量,创建暗色主题基础。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- uni-app项目已创建
|
|||
|
|
- Node.js环境已配置
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 1.1 创建目录结构
|
|||
|
|
```
|
|||
|
|
d:\uniapp\app_test\wwa3\
|
|||
|
|
├── components/
|
|||
|
|
│ ├── common/ # 通用组件
|
|||
|
|
│ ├── layout/ # 布局组件
|
|||
|
|
│ ├── panels/ # 面板组件
|
|||
|
|
│ └── drawers/ # 抽屉组件
|
|||
|
|
├── store/ # Pinia状态管理
|
|||
|
|
├── config/ # 游戏配置文件
|
|||
|
|
├── utils/ # 工具函数
|
|||
|
|
└── types/ # 类型定义(可选)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**操作步骤**:
|
|||
|
|
1. 在项目根目录执行:
|
|||
|
|
```bash
|
|||
|
|
mkdir -p components/common components/layout components/panels components/drawers
|
|||
|
|
mkdir -p store config utils types
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2 安装 Pinia
|
|||
|
|
```bash
|
|||
|
|
npm install pinia
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**预期输出**:package.json 中增加 `"pinia": "^2.x.x"`
|
|||
|
|
|
|||
|
|
#### 1.3 修改 main.js 引入 Pinia
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\main.js`
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```javascript
|
|||
|
|
import { createSSRApp } from 'vue'
|
|||
|
|
import { createPinia } from 'pinia'
|
|||
|
|
import App from './App.vue'
|
|||
|
|
|
|||
|
|
export function createApp() {
|
|||
|
|
const app = createSSRApp(App)
|
|||
|
|
const pinia = createPinia()
|
|||
|
|
app.use(pinia)
|
|||
|
|
return {
|
|||
|
|
app,
|
|||
|
|
pinia
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.4 修改 uni.scss 添加颜色变量
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\uni.scss`
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```scss
|
|||
|
|
/* 背景色 */
|
|||
|
|
$bg-primary: #0a0a0f;
|
|||
|
|
$bg-secondary: #1a1a2e;
|
|||
|
|
$bg-tertiary: #252542;
|
|||
|
|
|
|||
|
|
/* 文字色 */
|
|||
|
|
$text-primary: #e0e0e0;
|
|||
|
|
$text-secondary: #808080;
|
|||
|
|
$text-muted: #4a4a4a;
|
|||
|
|
|
|||
|
|
/* 强调色 */
|
|||
|
|
$accent: #4ecdc4;
|
|||
|
|
$danger: #ff6b6b;
|
|||
|
|
$warning: #ffe66d;
|
|||
|
|
$success: #4ade80;
|
|||
|
|
|
|||
|
|
/* 品质颜色 */
|
|||
|
|
$quality-trash: #808080;
|
|||
|
|
$quality-common: #ffffff;
|
|||
|
|
$quality-good: #4ade80;
|
|||
|
|
$quality-rare: #60a5fa;
|
|||
|
|
$quality-epic: #a855f7;
|
|||
|
|
$quality-legend: #f97316;
|
|||
|
|
|
|||
|
|
/* 边框色 */
|
|||
|
|
$border-color: #2a2a3e;
|
|||
|
|
$border-active: #4ecdc4;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.5 修改 App.vue 添加全局样式
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\App.vue`
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<script setup>
|
|||
|
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
|||
|
|
|
|||
|
|
onLaunch(() => {
|
|||
|
|
console.log('App Launch')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onShow(() => {
|
|||
|
|
console.log('App Show')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onHide(() => {
|
|||
|
|
console.log('App Hide')
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss">
|
|||
|
|
/* 全局样式 */
|
|||
|
|
@import '@/uni.scss';
|
|||
|
|
|
|||
|
|
page {
|
|||
|
|
background-color: $bg-primary;
|
|||
|
|
color: $text-primary;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 滚动条样式 */
|
|||
|
|
::-webkit-scrollbar {
|
|||
|
|
width: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::-webkit-scrollbar-thumb {
|
|||
|
|
background-color: $bg-tertiary;
|
|||
|
|
border-radius: 4rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 项目可正常运行(`npm run dev:mp-weixin` 或其他平台)
|
|||
|
|
- [ ] 浏览器控制台无错误
|
|||
|
|
- [ ] uni.scss 中的变量可以在组件中正常使用
|
|||
|
|
- [ ] 页面背景显示为深色(#0a0a0f)
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `components/common/` | 创建 | 目录 |
|
|||
|
|
| `components/layout/` | 创建 | 目录 |
|
|||
|
|
| `components/panels/` | 创建 | 目录 |
|
|||
|
|
| `components/drawers/` | 创建 | 目录 |
|
|||
|
|
| `store/` | 创建 | 目录 |
|
|||
|
|
| `config/` | 创建 | 目录 |
|
|||
|
|
| `utils/` | 创建 | 目录 |
|
|||
|
|
| `main.js` | 修改 | 引入Pinia |
|
|||
|
|
| `uni.scss` | 修改 | 添加颜色变量 |
|
|||
|
|
| `App.vue` | 修改 | 添加全局样式 |
|
|||
|
|
| `package.json` | 修改 | 增加pinia依赖 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 2: Pinia Store 结构设计
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
创建完整的 Pinia Store 结构,包括玩家状态、游戏状态、以及配置文件的框架。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1 完成
|
|||
|
|
- Pinia 已安装
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 2.1 创建玩家状态 Store
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\store\player.js`
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```javascript
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
|
|||
|
|
export const usePlayerStore = defineStore('player', () => {
|
|||
|
|
// 基础属性
|
|||
|
|
const baseStats = ref({
|
|||
|
|
strength: 10, // 力量
|
|||
|
|
agility: 8, // 敏捷
|
|||
|
|
dexterity: 8, // 灵巧
|
|||
|
|
intuition: 10, // 智力
|
|||
|
|
vitality: 10 // 体质(影响HP)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 当前资源
|
|||
|
|
const currentStats = ref({
|
|||
|
|
health: 100,
|
|||
|
|
maxHealth: 100,
|
|||
|
|
stamina: 100,
|
|||
|
|
maxStamina: 100,
|
|||
|
|
sanity: 100,
|
|||
|
|
maxSanity: 100
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 主等级
|
|||
|
|
const level = ref({
|
|||
|
|
current: 1,
|
|||
|
|
exp: 0,
|
|||
|
|
maxExp: 100
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 技能
|
|||
|
|
const skills = ref({})
|
|||
|
|
|
|||
|
|
// 装备
|
|||
|
|
const equipment = ref({
|
|||
|
|
weapon: null,
|
|||
|
|
armor: null,
|
|||
|
|
shield: null,
|
|||
|
|
accessory: null
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 背包
|
|||
|
|
const inventory = ref([])
|
|||
|
|
|
|||
|
|
// 货币(以铜币为单位)
|
|||
|
|
const currency = ref({
|
|||
|
|
copper: 0
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 当前位置
|
|||
|
|
const currentLocation = ref('camp')
|
|||
|
|
|
|||
|
|
// 计算属性:总货币
|
|||
|
|
const totalCurrency = computed(() => {
|
|||
|
|
return {
|
|||
|
|
gold: Math.floor(currency.value.copper / 10000),
|
|||
|
|
silver: Math.floor((currency.value.copper % 10000) / 100),
|
|||
|
|
copper: currency.value.copper % 100
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 重置玩家数据
|
|||
|
|
function resetPlayer() {
|
|||
|
|
// 重置逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
baseStats,
|
|||
|
|
currentStats,
|
|||
|
|
level,
|
|||
|
|
skills,
|
|||
|
|
equipment,
|
|||
|
|
inventory,
|
|||
|
|
currency,
|
|||
|
|
totalCurrency,
|
|||
|
|
currentLocation,
|
|||
|
|
resetPlayer
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2 创建游戏状态 Store
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\store\game.js`
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```javascript
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref } from 'vue'
|
|||
|
|
|
|||
|
|
export const useGameStore = defineStore('game', () => {
|
|||
|
|
// 当前标签页
|
|||
|
|
const currentTab = ref('status')
|
|||
|
|
|
|||
|
|
// 抽屉状态
|
|||
|
|
const drawerState = ref({
|
|||
|
|
inventory: false,
|
|||
|
|
event: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 日志
|
|||
|
|
const logs = ref([])
|
|||
|
|
|
|||
|
|
// 游戏时间
|
|||
|
|
const gameTime = ref({
|
|||
|
|
day: 1,
|
|||
|
|
hour: 8,
|
|||
|
|
minute: 0,
|
|||
|
|
totalMinutes: 480
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 战斗状态
|
|||
|
|
const inCombat = ref(false)
|
|||
|
|
const combatState = ref(null)
|
|||
|
|
|
|||
|
|
// 活动任务
|
|||
|
|
const activeTasks = ref([])
|
|||
|
|
|
|||
|
|
// 负面状态
|
|||
|
|
const negativeStatus = ref([])
|
|||
|
|
|
|||
|
|
// 市场价格
|
|||
|
|
const marketPrices = ref({
|
|||
|
|
lastRefreshDay: 1,
|
|||
|
|
prices: {}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 添加日志
|
|||
|
|
function addLog(message, type = 'info') {
|
|||
|
|
const time = `${String(gameTime.value.hour).padStart(2, '0')}:${String(gameTime.value.minute).padStart(2, '0')}`
|
|||
|
|
logs.value.push({
|
|||
|
|
id: Date.now(),
|
|||
|
|
time,
|
|||
|
|
message,
|
|||
|
|
type
|
|||
|
|
})
|
|||
|
|
// 限制日志数量
|
|||
|
|
if (logs.value.length > 200) {
|
|||
|
|
logs.value.shift()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重置游戏状态
|
|||
|
|
function resetGame() {
|
|||
|
|
// 重置逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
currentTab,
|
|||
|
|
drawerState,
|
|||
|
|
logs,
|
|||
|
|
gameTime,
|
|||
|
|
inCombat,
|
|||
|
|
combatState,
|
|||
|
|
activeTasks,
|
|||
|
|
negativeStatus,
|
|||
|
|
marketPrices,
|
|||
|
|
addLog,
|
|||
|
|
resetGame
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.3 创建配置文件框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\constants.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 游戏数值常量
|
|||
|
|
export const GAME_CONSTANTS = {
|
|||
|
|
// 时间流速:现实1秒 = 游戏时间5分钟
|
|||
|
|
TIME_SCALE: 5,
|
|||
|
|
|
|||
|
|
// 离线收益时间(小时)
|
|||
|
|
OFFLINE_HOURS_BASE: 0.5,
|
|||
|
|
OFFLINE_HOURS_AD: 2.5,
|
|||
|
|
|
|||
|
|
// 耐力相关
|
|||
|
|
STAMINA_FULL_EFFECT: 50, // 耐力高于此值时无惩罚
|
|||
|
|
STAMINA_EMPTY_PENALTY: 0.5, // 耐力耗尽时效率减半
|
|||
|
|
|
|||
|
|
// 经验倍率
|
|||
|
|
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
|
|||
|
|
|
|||
|
|
// 品质等级
|
|||
|
|
QUALITY_LEVELS: {
|
|||
|
|
1: { name: '垃圾', color: '#808080', range: [0, 49], multiplier: 1.0 },
|
|||
|
|
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
|||
|
|
3: { name: '优秀', color: '#4ade80', range: [100, 129], multiplier: 1.1 },
|
|||
|
|
4: { name: '稀有', color: '#60a5fa', range: [130, 159], multiplier: 1.3 },
|
|||
|
|
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 1.6 },
|
|||
|
|
6: { name: '传说', color: '#f97316', range: [200, 250], multiplier: 2.0 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.4 创建技能配置框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\skills.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 技能配置
|
|||
|
|
export const SKILL_CONFIG = {
|
|||
|
|
// 战斗技能
|
|||
|
|
stick_mastery: {
|
|||
|
|
id: 'stick_mastery',
|
|||
|
|
name: '木棍精通',
|
|||
|
|
type: 'combat',
|
|||
|
|
category: 'weapon',
|
|||
|
|
icon: '',
|
|||
|
|
maxLevel: 20,
|
|||
|
|
expPerLevel: (level) => level * 100,
|
|||
|
|
milestones: {
|
|||
|
|
5: { desc: '所有武器暴击率+2%', effect: { critRate: 2 } },
|
|||
|
|
10: { desc: '所有武器攻击力+5%', effect: { attackBonus: 5 } },
|
|||
|
|
15: { desc: '武器熟练度获取速度+20%', effect: { expRate: 1.2 } },
|
|||
|
|
20: { desc: '所有武器暴击伤害+0.3', effect: { critMult: 0.3 } }
|
|||
|
|
},
|
|||
|
|
unlockCondition: null, // 初始解锁
|
|||
|
|
unlockItem: 'wooden_stick'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
reading: {
|
|||
|
|
id: 'reading',
|
|||
|
|
name: '阅读',
|
|||
|
|
type: 'life',
|
|||
|
|
category: 'reading',
|
|||
|
|
icon: '',
|
|||
|
|
maxLevel: 20,
|
|||
|
|
expPerLevel: (level) => level * 50,
|
|||
|
|
milestones: {
|
|||
|
|
3: { desc: '所有技能经验获取+5%', effect: { globalExpRate: 5 } },
|
|||
|
|
5: { desc: '阅读速度+50%', effect: { readingSpeed: 1.5 } },
|
|||
|
|
10: { desc: '完成书籍给予额外主经验+100', effect: { bookExpBonus: 100 } }
|
|||
|
|
},
|
|||
|
|
unlockCondition: null,
|
|||
|
|
unlockItem: 'old_book'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
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' }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.5 创建物品配置框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\items.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 物品配置
|
|||
|
|
export const ITEM_CONFIG = {
|
|||
|
|
// 武器
|
|||
|
|
wooden_stick: {
|
|||
|
|
id: 'wooden_stick',
|
|||
|
|
name: '木棍',
|
|||
|
|
type: 'weapon',
|
|||
|
|
subtype: 'one_handed',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 10, // 基础价值(铜币)
|
|||
|
|
baseDamage: 5,
|
|||
|
|
attackSpeed: 1.0,
|
|||
|
|
quality: 100, // 默认品质
|
|||
|
|
unlockSkill: 'stick_mastery',
|
|||
|
|
description: '一根粗糙的木棍,至少比空手强。'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 消耗品
|
|||
|
|
bread: {
|
|||
|
|
id: 'bread',
|
|||
|
|
name: '面包',
|
|||
|
|
type: 'consumable',
|
|||
|
|
subtype: 'food',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 10,
|
|||
|
|
effect: {
|
|||
|
|
stamina: 20
|
|||
|
|
},
|
|||
|
|
description: '普通的面包,可以恢复耐力。',
|
|||
|
|
stackable: true,
|
|||
|
|
maxStack: 99
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
healing_herb: {
|
|||
|
|
id: 'healing_herb',
|
|||
|
|
name: '草药',
|
|||
|
|
type: 'consumable',
|
|||
|
|
subtype: 'medicine',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 15,
|
|||
|
|
effect: {
|
|||
|
|
health: 15
|
|||
|
|
},
|
|||
|
|
description: '常见的治疗草药,可以恢复生命值。',
|
|||
|
|
stackable: true,
|
|||
|
|
maxStack: 99
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 书籍
|
|||
|
|
old_book: {
|
|||
|
|
id: 'old_book',
|
|||
|
|
name: '破旧书籍',
|
|||
|
|
type: 'book',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 50,
|
|||
|
|
readingTime: 60, // 秒
|
|||
|
|
expReward: {
|
|||
|
|
reading: 10
|
|||
|
|
},
|
|||
|
|
completionBonus: null,
|
|||
|
|
description: '一本破旧的书籍,记录着一些基础知识。',
|
|||
|
|
consumable: false // 书籍不消耗
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 素材
|
|||
|
|
dog_skin: {
|
|||
|
|
id: 'dog_skin',
|
|||
|
|
name: '狗皮',
|
|||
|
|
type: 'material',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 5,
|
|||
|
|
description: '野狗的皮毛,可以用来制作简单装备。',
|
|||
|
|
stackable: true,
|
|||
|
|
maxStack: 99
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 关键道具
|
|||
|
|
basement_key: {
|
|||
|
|
id: 'basement_key',
|
|||
|
|
name: '地下室钥匙',
|
|||
|
|
type: 'key',
|
|||
|
|
icon: '',
|
|||
|
|
baseValue: 0,
|
|||
|
|
description: '一把生锈的钥匙,上面刻着「B」字母。',
|
|||
|
|
stackable: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.6 创建区域配置框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\locations.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 区域配置
|
|||
|
|
export const LOCATION_CONFIG = {
|
|||
|
|
camp: {
|
|||
|
|
id: 'camp',
|
|||
|
|
name: '测试营地',
|
|||
|
|
type: 'safe',
|
|||
|
|
environment: 'normal',
|
|||
|
|
description: '一个临时的幸存者营地,相对安全。',
|
|||
|
|
connections: ['market', 'blackmarket', 'wild1'],
|
|||
|
|
npcs: ['injured_adventurer'],
|
|||
|
|
activities: ['rest', 'talk']
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
market: {
|
|||
|
|
id: 'market',
|
|||
|
|
name: '测试市场',
|
|||
|
|
type: 'safe',
|
|||
|
|
environment: 'normal',
|
|||
|
|
description: '商人老张在这里摆摊。',
|
|||
|
|
connections: ['camp'],
|
|||
|
|
npcs: ['merchant_zhang'],
|
|||
|
|
activities: ['trade']
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
blackmarket: {
|
|||
|
|
id: 'blackmarket',
|
|||
|
|
name: '测试黑市',
|
|||
|
|
type: 'safe',
|
|||
|
|
environment: 'normal',
|
|||
|
|
description: '神秘人偶尔会在这里出现。',
|
|||
|
|
connections: ['camp'],
|
|||
|
|
npcs: ['mysterious_man'],
|
|||
|
|
activities: ['trade']
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
wild1: {
|
|||
|
|
id: 'wild1',
|
|||
|
|
name: '测试野外1',
|
|||
|
|
type: 'danger',
|
|||
|
|
environment: 'normal',
|
|||
|
|
description: '野狗经常出没的区域。',
|
|||
|
|
connections: ['camp', 'boss_lair'],
|
|||
|
|
enemies: ['wild_dog'],
|
|||
|
|
activities: ['explore', 'combat']
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
boss_lair: {
|
|||
|
|
id: 'boss_lair',
|
|||
|
|
name: '测试Boss巢',
|
|||
|
|
type: 'danger',
|
|||
|
|
environment: 'normal',
|
|||
|
|
description: '强大的野兽盘踞在这里。',
|
|||
|
|
connections: ['wild1', 'basement'],
|
|||
|
|
enemies: ['test_boss'],
|
|||
|
|
unlockCondition: { type: 'kill', target: 'wild_dog', count: 5 },
|
|||
|
|
activities: ['combat']
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
basement: {
|
|||
|
|
id: 'basement',
|
|||
|
|
name: '地下室',
|
|||
|
|
type: 'dungeon',
|
|||
|
|
environment: 'dark',
|
|||
|
|
description: '黑暗潮湿的地下室,需要照明。',
|
|||
|
|
connections: ['boss_lair'],
|
|||
|
|
unlockCondition: { type: 'item', item: 'basement_key' },
|
|||
|
|
activities: ['explore', 'read']
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.7 创建敌人配置框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\enemies.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 敌人配置
|
|||
|
|
export const ENEMY_CONFIG = {
|
|||
|
|
wild_dog: {
|
|||
|
|
id: 'wild_dog',
|
|||
|
|
name: '野狗',
|
|||
|
|
level: 1,
|
|||
|
|
baseStats: {
|
|||
|
|
health: 30,
|
|||
|
|
attack: 8,
|
|||
|
|
defense: 2,
|
|||
|
|
speed: 1.0
|
|||
|
|
},
|
|||
|
|
derivedStats: {
|
|||
|
|
ap: 10, // 攻击点数
|
|||
|
|
ep: 8 // 闪避点数
|
|||
|
|
},
|
|||
|
|
expReward: 15,
|
|||
|
|
drops: [
|
|||
|
|
{ item: 'dog_skin', chance: 0.8, min: 1, max: 1 },
|
|||
|
|
{ item: 'copper', chance: 1.0, min: 5, max: 15 }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
test_boss: {
|
|||
|
|
id: 'test_boss',
|
|||
|
|
name: '测试Boss',
|
|||
|
|
level: 5,
|
|||
|
|
baseStats: {
|
|||
|
|
health: 200,
|
|||
|
|
attack: 25,
|
|||
|
|
defense: 10,
|
|||
|
|
speed: 0.8
|
|||
|
|
},
|
|||
|
|
derivedStats: {
|
|||
|
|
ap: 25,
|
|||
|
|
ep: 15
|
|||
|
|
},
|
|||
|
|
expReward: 150,
|
|||
|
|
drops: [
|
|||
|
|
{ item: 'basement_key', chance: 1.0, min: 1, max: 1 },
|
|||
|
|
{ item: 'copper', chance: 1.0, min: 50, max: 100 }
|
|||
|
|
],
|
|||
|
|
isBoss: true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.8 创建NPC配置框架
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\config\npcs.js`
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// NPC配置
|
|||
|
|
export const NPC_CONFIG = {
|
|||
|
|
injured_adventurer: {
|
|||
|
|
id: 'injured_adventurer',
|
|||
|
|
name: '受伤的冒险者',
|
|||
|
|
location: 'camp',
|
|||
|
|
dialogue: {
|
|||
|
|
first: {
|
|||
|
|
text: '你终于醒了...这里是营地,暂时安全。外面的世界很危险,带上这根木棍防身吧。',
|
|||
|
|
choices: [
|
|||
|
|
{ text: '谢谢', next: 'thanks' },
|
|||
|
|
{ text: '这是什么地方?', next: 'explain' }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
thanks: {
|
|||
|
|
text: '小心野狗,它们通常成群出现。如果你能击败五只野狗,或许能找到通往深处的路。',
|
|||
|
|
choices: [
|
|||
|
|
{ text: '明白了', next: null, action: 'give_stick' }
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
explain: {
|
|||
|
|
text: '这是末世后的世界...具体细节我也记不清了。总之,活下去是第一要务。',
|
|||
|
|
choices: [
|
|||
|
|
{ text: '我会的', next: 'thanks', action: 'give_stick' }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
merchant_zhang: {
|
|||
|
|
id: 'merchant_zhang',
|
|||
|
|
name: '商人老张',
|
|||
|
|
location: 'market',
|
|||
|
|
dialogue: {
|
|||
|
|
first: {
|
|||
|
|
text: '欢迎光临!看看有什么需要的?',
|
|||
|
|
choices: [
|
|||
|
|
{ text: '查看商品', next: null, action: 'open_shop' },
|
|||
|
|
{ text: '离开', next: null }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] Store 文件可以正常导入
|
|||
|
|
- [ ] usePlayerStore 和 useGameStore 可以在组件中使用
|
|||
|
|
- [ ] 配置文件结构完整,无语法错误
|
|||
|
|
- [ ] 游戏常量可以正确访问
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `store/player.js` | 创建 | 玩家状态Store |
|
|||
|
|
| `store/game.js` | 创建 | 游戏状态Store |
|
|||
|
|
| `config/constants.js` | 创建 | 游戏常量 |
|
|||
|
|
| `config/skills.js` | 创建 | 技能配置 |
|
|||
|
|
| `config/items.js` | 创建 | 物品配置 |
|
|||
|
|
| `config/locations.js` | 创建 | 区域配置 |
|
|||
|
|
| `config/enemies.js` | 创建 | 敌人配置 |
|
|||
|
|
| `config/npcs.js` | 创建 | NPC配置 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 3: 通用UI组件开发
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
开发所有基础UI组件,包括进度条、属性显示、按钮、折叠面板、标签组、底部导航和抽屉。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1 完成
|
|||
|
|
- Phase 2 完成(Store可用)
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 3.1 创建进度条组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\common\ProgressBar.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| value | Number | 0 | 当前值 |
|
|||
|
|
| max | Number | 100 | 最大值 |
|
|||
|
|
| color | String | '$accent' | 进度条颜色 |
|
|||
|
|
| showText | Boolean | true | 显示数值文本 |
|
|||
|
|
| height | String | '16rpx' | 进度条高度 |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="progress-bar" :style="{ height }">
|
|||
|
|
<view class="progress-bar__fill" :style="fillStyle"></view>
|
|||
|
|
<view v-if="showText" class="progress-bar__text">
|
|||
|
|
{{ value }}/{{ max }}
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
value: { type: Number, default: 0 },
|
|||
|
|
max: { type: Number, default: 100 },
|
|||
|
|
color: { type: String, default: '#4ecdc4' },
|
|||
|
|
showText: { type: Boolean, default: true },
|
|||
|
|
height: { type: String, default: '16rpx' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const percentage = computed(() => {
|
|||
|
|
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const fillStyle = computed(() => ({
|
|||
|
|
width: `${percentage.value}%`,
|
|||
|
|
backgroundColor: props.color
|
|||
|
|
}))
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.progress-bar {
|
|||
|
|
position: relative;
|
|||
|
|
background-color: $bg-tertiary;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
&__fill {
|
|||
|
|
height: 100%;
|
|||
|
|
transition: width 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__text {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 50%;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
font-size: 20rpx;
|
|||
|
|
color: $text-primary;
|
|||
|
|
text-shadow: 0 0 4rpx rgba(0,0,0,0.8);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2 创建属性显示行组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\common\StatItem.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| label | String | '' | 属性名称 |
|
|||
|
|
| value | Number,String | '' | 属性值 |
|
|||
|
|
| icon | String | '' | 图标(可选) |
|
|||
|
|
| color | String | '$text-primary' | 文字颜色 |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="stat-item">
|
|||
|
|
<text v-if="icon" class="stat-item__icon">{{ icon }}</text>
|
|||
|
|
<text class="stat-item__label">{{ label }}</text>
|
|||
|
|
<text class="stat-item__value" :style="{ color }">{{ value }}</text>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
defineProps({
|
|||
|
|
label: String,
|
|||
|
|
value: [Number, String],
|
|||
|
|
icon: String,
|
|||
|
|
color: { type: String, default: '#e0e0e0' }
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.stat-item {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
padding: 8rpx 16rpx;
|
|||
|
|
|
|||
|
|
&__icon { font-size: 28rpx; }
|
|||
|
|
&__label { color: $text-secondary; }
|
|||
|
|
&__value { font-weight: bold; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.3 创建文字按钮组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\common\TextButton.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| text | String | '' | 按钮文字 |
|
|||
|
|
| type | String | 'default' | 类型:default/danger/warning |
|
|||
|
|
| disabled | Boolean | false | 是否禁用 |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view
|
|||
|
|
class="text-button"
|
|||
|
|
:class="[`text-button--${type}`, { 'text-button--disabled': disabled }]"
|
|||
|
|
@click="handleClick"
|
|||
|
|
>
|
|||
|
|
<text>{{ text }}</text>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
const props = defineProps({
|
|||
|
|
text: { type: String, required: true },
|
|||
|
|
type: { type: String, default: 'default' },
|
|||
|
|
disabled: { type: Boolean, default: false }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['click'])
|
|||
|
|
|
|||
|
|
function handleClick() {
|
|||
|
|
if (!props.disabled) {
|
|||
|
|
emit('click')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.text-button {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 16rpx 32rpx;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
|
|||
|
|
&--default {
|
|||
|
|
background-color: $bg-tertiary;
|
|||
|
|
color: $text-primary;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&--danger {
|
|||
|
|
background-color: rgba($danger, 0.2);
|
|||
|
|
color: $danger;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&--warning {
|
|||
|
|
background-color: rgba($warning, 0.2);
|
|||
|
|
color: $warning;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&--disabled {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.4 创建折叠面板组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\common\Collapse.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| title | String | '' | 折叠标题 |
|
|||
|
|
| expanded | Boolean | false | 是否展开 |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="collapse">
|
|||
|
|
<view class="collapse__header" @click="toggle">
|
|||
|
|
<text class="collapse__title">{{ title }}</text>
|
|||
|
|
<text class="collapse__arrow">{{ expanded ? '▼' : '▶' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-show="expanded" class="collapse__content">
|
|||
|
|
<slot></slot>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
title: { type: String, default: '' },
|
|||
|
|
expanded: { type: Boolean, default: false }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['toggle'])
|
|||
|
|
|
|||
|
|
const isExpanded = ref(props.expanded)
|
|||
|
|
|
|||
|
|
function toggle() {
|
|||
|
|
isExpanded.value = !isExpanded.value
|
|||
|
|
emit('toggle', isExpanded.value)
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.collapse {
|
|||
|
|
border-top: 1rpx solid $border-color;
|
|||
|
|
|
|||
|
|
&__header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 16rpx;
|
|||
|
|
background-color: $bg-secondary;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__content {
|
|||
|
|
padding: 16rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.5 创建筛选标签组组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\common\FilterTabs.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| tabs | Array | [] | 标签列表 [{id, label}] |
|
|||
|
|
| modelValue | String | '' | 当前选中ID |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="filter-tabs">
|
|||
|
|
<view
|
|||
|
|
v-for="tab in tabs"
|
|||
|
|
:key="tab.id"
|
|||
|
|
class="filter-tabs__item"
|
|||
|
|
:class="{ 'filter-tabs__item--active': modelValue === tab.id }"
|
|||
|
|
@click="$emit('update:modelValue', tab.id)"
|
|||
|
|
>
|
|||
|
|
<text>{{ tab.label }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
defineProps({
|
|||
|
|
tabs: { type: Array, default: () => [] },
|
|||
|
|
modelValue: { type: String, default: '' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
defineEmits(['update:modelValue'])
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.filter-tabs {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8rpx;
|
|||
|
|
padding: 8rpx;
|
|||
|
|
|
|||
|
|
&__item {
|
|||
|
|
padding: 12rpx 20rpx;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
background-color: $bg-tertiary;
|
|||
|
|
color: $text-secondary;
|
|||
|
|
|
|||
|
|
&--active {
|
|||
|
|
background-color: $accent;
|
|||
|
|
color: $bg-primary;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.6 创建底部导航栏组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\layout\TabBar.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| tabs | Array | [] | 导航项 [{id, label, icon}] |
|
|||
|
|
| modelValue | String | '' | 当前选中ID |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="tab-bar">
|
|||
|
|
<view
|
|||
|
|
v-for="tab in tabs"
|
|||
|
|
:key="tab.id"
|
|||
|
|
class="tab-bar__item"
|
|||
|
|
:class="{ 'tab-bar__item--active': modelValue === tab.id }"
|
|||
|
|
@click="$emit('update:modelValue', tab.id)"
|
|||
|
|
>
|
|||
|
|
<text v-if="tab.icon" class="tab-bar__icon">{{ tab.icon }}</text>
|
|||
|
|
<text class="tab-bar__label">{{ tab.label }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
defineProps({
|
|||
|
|
tabs: { type: Array, default: () => [] },
|
|||
|
|
modelValue: { type: String, default: '' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
defineEmits(['update:modelValue'])
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.tab-bar {
|
|||
|
|
display: flex;
|
|||
|
|
height: 100rpx;
|
|||
|
|
background-color: $bg-secondary;
|
|||
|
|
border-top: 1rpx solid $border-color;
|
|||
|
|
|
|||
|
|
&__item {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 4rpx;
|
|||
|
|
color: $text-secondary;
|
|||
|
|
|
|||
|
|
&--active {
|
|||
|
|
color: $accent;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&__icon { font-size: 32rpx; }
|
|||
|
|
&__label { font-size: 20rpx; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.7 创建右侧抽屉组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\layout\Drawer.vue`
|
|||
|
|
|
|||
|
|
**Props规范**:
|
|||
|
|
| Prop | Type | Default | Description |
|
|||
|
|
|------|------|---------|-------------|
|
|||
|
|
| visible | Boolean | false | 是否显示 |
|
|||
|
|
| width | String | '600rpx' | 抽屉宽度 |
|
|||
|
|
|
|||
|
|
**代码要点**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view v-if="visible" class="drawer" @click="handleMaskClick">
|
|||
|
|
<view class="drawer__content" :style="{ width }" @click.stop>
|
|||
|
|
<slot></slot>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
const props = defineProps({
|
|||
|
|
visible: Boolean,
|
|||
|
|
width: { type: String, default: '600rpx' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['close'])
|
|||
|
|
|
|||
|
|
function handleMaskClick() {
|
|||
|
|
emit('close')
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.drawer {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background-color: rgba(0,0,0,0.6);
|
|||
|
|
z-index: 999;
|
|||
|
|
|
|||
|
|
&__content {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background-color: $bg-secondary;
|
|||
|
|
border-left: 1rpx solid $border-color;
|
|||
|
|
animation: slideIn 0.3s ease;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideIn {
|
|||
|
|
from { transform: translateX(100%); }
|
|||
|
|
to { transform: translateX(0); }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] ProgressBar 可以正确显示进度百分比
|
|||
|
|
- [ ] StatItem 可以正确显示属性值
|
|||
|
|
- [ ] TextButton 点击时有反馈
|
|||
|
|
- [ ] Collapse 可以展开/收起
|
|||
|
|
- [ ] FilterTabs 可以切换选中状态
|
|||
|
|
- [ ] TabBar 可以切换并高亮当前项
|
|||
|
|
- [ ] Drawer 可以从右侧滑入,点击遮罩关闭
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `components/common/ProgressBar.vue` | 创建 | 进度条组件 |
|
|||
|
|
| `components/common/StatItem.vue` | 创建 | 属性显示组件 |
|
|||
|
|
| `components/common/TextButton.vue` | 创建 | 文字按钮组件 |
|
|||
|
|
| `components/common/Collapse.vue` | 创建 | 折叠面板组件 |
|
|||
|
|
| `components/common/FilterTabs.vue` | 创建 | 筛选标签组件 |
|
|||
|
|
| `components/layout/TabBar.vue` | 创建 | 底部导航组件 |
|
|||
|
|
| `components/layout/Drawer.vue` | 创建 | 右侧抽屉组件 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 4: 核心面板组件开发
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
开发三大主面板(状态面板、地图面板、日志面板)和主页面容器。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1 完成
|
|||
|
|
- Phase 2 完成(Store可用)
|
|||
|
|
- Phase 3 完成(通用组件可用)
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 4.1 创建状态面板组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\panels\StatusPanel.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 上区:HP/耐力/精神进度条
|
|||
|
|
- 主属性摘要
|
|||
|
|
- 货币显示
|
|||
|
|
- Collapse展开更多属性
|
|||
|
|
- FilterTabs技能筛选
|
|||
|
|
- 技能列表scroll-view
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="status-panel">
|
|||
|
|
<!-- 资源进度条 -->
|
|||
|
|
<view class="status-panel__resources">
|
|||
|
|
<ProgressBar label="HP" :value="player.currentStats.health" :max="player.currentStats.maxHealth" color="#ff6b6b" />
|
|||
|
|
<ProgressBar label="耐力" :value="player.currentStats.stamina" :max="player.currentStats.maxStamina" color="#ffe66d" />
|
|||
|
|
<ProgressBar label="精神" :value="player.currentStats.sanity" :max="player.currentStats.maxSanity" color="#4ecdc4" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 主属性摘要 -->
|
|||
|
|
<view class="status-panel__stats">
|
|||
|
|
<StatItem label="力量" :value="player.baseStats.strength" />
|
|||
|
|
<StatItem label="敏捷" :value="player.baseStats.agility" />
|
|||
|
|
<StatItem label="灵巧" :value="player.baseStats.dexterity" />
|
|||
|
|
<StatItem label="智力" :value="player.baseStats.intuition" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 货币 -->
|
|||
|
|
<view class="status-panel__currency">
|
|||
|
|
<text>💰 {{ currencyText }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 更多属性折叠 -->
|
|||
|
|
<Collapse title="▼ 更多属性" :expanded="showMoreStats" @toggle="showMoreStats = $event">
|
|||
|
|
<view class="status-panel__more-stats">
|
|||
|
|
<StatItem label="攻击力" :value="finalStats.attack" />
|
|||
|
|
<StatItem label="防御力" :value="finalStats.defense" />
|
|||
|
|
<StatItem label="暴击率" :value="finalStats.critRate + '%'" />
|
|||
|
|
<StatItem label="AP" :value="finalStats.ap" />
|
|||
|
|
<StatItem label="EP" :value="finalStats.ep" />
|
|||
|
|
</view>
|
|||
|
|
</Collapse>
|
|||
|
|
|
|||
|
|
<!-- 技能筛选 -->
|
|||
|
|
<FilterTabs
|
|||
|
|
:tabs="skillFilterTabs"
|
|||
|
|
v-model="currentSkillFilter"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 技能列表 -->
|
|||
|
|
<scroll-view class="status-panel__skills" scroll-y>
|
|||
|
|
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
|
|||
|
|
<text class="skill-item__icon">{{ skill.icon }}</text>
|
|||
|
|
<text class="skill-item__name">{{ skill.name }}</text>
|
|||
|
|
<text class="skill-item__level">Lv.{{ skill.level }}</text>
|
|||
|
|
<ProgressBar :value="skill.exp" :max="skill.maxExp" height="8rpx" />
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed, ref } from 'vue'
|
|||
|
|
import { usePlayerStore } from '@/store/player'
|
|||
|
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
|||
|
|
import StatItem from '@/components/common/StatItem.vue'
|
|||
|
|
import Collapse from '@/components/common/Collapse.vue'
|
|||
|
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
|||
|
|
|
|||
|
|
const player = usePlayerStore()
|
|||
|
|
const showMoreStats = ref(false)
|
|||
|
|
const currentSkillFilter = ref('all')
|
|||
|
|
|
|||
|
|
const skillFilterTabs = [
|
|||
|
|
{ id: 'all', label: '全部' },
|
|||
|
|
{ id: 'combat', label: '战斗' },
|
|||
|
|
{ id: 'life', label: '生活' },
|
|||
|
|
{ id: 'passive', label: '被动' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const currencyText = computed(() => {
|
|||
|
|
const c = player.currency.copper
|
|||
|
|
const gold = Math.floor(c / 10000)
|
|||
|
|
const silver = Math.floor((c % 10000) / 100)
|
|||
|
|
const copper = c % 100
|
|||
|
|
return `${gold}金 ${silver}银 ${copper}铜`
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const filteredSkills = computed(() => {
|
|||
|
|
// 返回筛选后的技能列表
|
|||
|
|
return []
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const finalStats = computed(() => {
|
|||
|
|
// 计算最终属性
|
|||
|
|
return { attack: 0, defense: 0, critRate: 5, ap: 0, ep: 0 }
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.status-panel {
|
|||
|
|
padding: 16rpx;
|
|||
|
|
// 具体样式
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 创建地图面板组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\panels\MapPanel.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 顶部当前位置显示
|
|||
|
|
- 战斗状态区(战斗时显示)
|
|||
|
|
- 区域列表scroll-view
|
|||
|
|
- 活动按钮组
|
|||
|
|
- 背包按钮唤起抽屉
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="map-panel">
|
|||
|
|
<!-- 顶部状态 -->
|
|||
|
|
<view class="map-panel__header">
|
|||
|
|
<text class="location-name">📍 {{ currentLocation.name }}</text>
|
|||
|
|
<TextButton text="📦 背包" @click="openInventory" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 战斗状态 -->
|
|||
|
|
<view v-if="game.inCombat" class="combat-status">
|
|||
|
|
<text>🐕 {{ combatState.enemyName }}</text>
|
|||
|
|
<ProgressBar :value="combatState.enemyHp" :max="combatState.enemyMaxHp" color="#ff6b6b" />
|
|||
|
|
<FilterTabs
|
|||
|
|
:tabs="stanceTabs"
|
|||
|
|
v-model="combatState.stance"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 区域列表 -->
|
|||
|
|
<scroll-view class="map-panel__locations" scroll-y>
|
|||
|
|
<view
|
|||
|
|
v-for="loc in availableLocations"
|
|||
|
|
:key="loc.id"
|
|||
|
|
class="location-item"
|
|||
|
|
:class="{ 'location-item--locked': loc.locked }"
|
|||
|
|
@click="tryTravel(loc)"
|
|||
|
|
>
|
|||
|
|
<text class="location-item__name">{{ loc.name }}</text>
|
|||
|
|
<text class="location-item__type">[{{ loc.typeText }}]</text>
|
|||
|
|
<text v-if="loc.locked" class="location-item__lock">🔒</text>
|
|||
|
|
<text v-else class="location-item__arrow">→</text>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 活动按钮 -->
|
|||
|
|
<view class="map-panel__actions">
|
|||
|
|
<TextButton
|
|||
|
|
v-for="action in currentActions"
|
|||
|
|
:key="action.id"
|
|||
|
|
:text="action.label"
|
|||
|
|
@click="doAction(action.id)"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed } from 'vue'
|
|||
|
|
import { useGameStore } from '@/store/game'
|
|||
|
|
import { usePlayerStore } from '@/store/player'
|
|||
|
|
import { LOCATION_CONFIG } from '@/config/locations'
|
|||
|
|
import TextButton from '@/components/common/TextButton.vue'
|
|||
|
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
|||
|
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
|||
|
|
|
|||
|
|
const game = useGameStore()
|
|||
|
|
const player = usePlayerStore()
|
|||
|
|
|
|||
|
|
const stanceTabs = [
|
|||
|
|
{ id: 'attack', label: '攻击' },
|
|||
|
|
{ id: 'defense', label: '防御' },
|
|||
|
|
{ id: 'balance', label: '平衡' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const currentLocation = computed(() => {
|
|||
|
|
return LOCATION_CONFIG[player.currentLocation] || { name: '未知' }
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const availableLocations = computed(() => {
|
|||
|
|
// 返回可前往的区域列表
|
|||
|
|
return []
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const currentActions = computed(() => {
|
|||
|
|
// 返回当前区域可用的活动
|
|||
|
|
return []
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function openInventory() {
|
|||
|
|
game.drawerState.inventory = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function tryTravel(location) {
|
|||
|
|
// 尝试前往区域
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function doAction(actionId) {
|
|||
|
|
// 执行活动
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.map-panel {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100%;
|
|||
|
|
// 具体样式
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.3 创建日志面板组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\panels\LogPanel.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- scroll-view显示日志
|
|||
|
|
- 自动滚动到底部
|
|||
|
|
- 不同类型日志的样式区分
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<scroll-view
|
|||
|
|
class="log-panel"
|
|||
|
|
scroll-y
|
|||
|
|
:scroll-into-view="scrollToId"
|
|||
|
|
:scroll-with-animation="true"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
v-for="log in logs"
|
|||
|
|
:key="log.id"
|
|||
|
|
class="log-item"
|
|||
|
|
:class="`log-item--${log.type}`"
|
|||
|
|
:id="'log-' + log.id"
|
|||
|
|
>
|
|||
|
|
<text class="log-item__time">[{{ log.time }}]</text>
|
|||
|
|
<text class="log-item__message">{{ log.message }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view :id="'log-anchor-' + lastLogId" style="height: 1rpx;"></view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed, watch, nextTick, ref } from 'vue'
|
|||
|
|
import { useGameStore } from '@/store/game'
|
|||
|
|
|
|||
|
|
const game = useGameStore()
|
|||
|
|
const scrollToId = ref('')
|
|||
|
|
|
|||
|
|
const logs = computed(() => game.logs)
|
|||
|
|
|
|||
|
|
const lastLogId = computed(() => {
|
|||
|
|
return logs.value.length > 0 ? logs.value[logs.value.length - 1].id : ''
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
watch(lastLogId, (newId) => {
|
|||
|
|
if (newId) {
|
|||
|
|
nextTick(() => {
|
|||
|
|
scrollToId.value = 'log-anchor-' + newId
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}, { immediate: true })
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.log-panel {
|
|||
|
|
height: 100%;
|
|||
|
|
padding: 16rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item {
|
|||
|
|
padding: 8rpx 0;
|
|||
|
|
border-bottom: 1rpx solid $bg-tertiary;
|
|||
|
|
|
|||
|
|
&__time { color: $text-secondary; margin-right: 8rpx; }
|
|||
|
|
&__message { color: $text-primary; }
|
|||
|
|
|
|||
|
|
&--combat { color: $danger; }
|
|||
|
|
&--system { color: $accent; }
|
|||
|
|
&--reward { color: $warning; }
|
|||
|
|
&--info { color: $text-secondary; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.4 重构主页面容器
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\pages\index\index.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 顶部标题栏(显示游戏时间)
|
|||
|
|
- v-show 面板切换
|
|||
|
|
- TabBar 集成
|
|||
|
|
- Drawer 集成
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="game-container">
|
|||
|
|
<!-- 顶部标题栏 -->
|
|||
|
|
<view class="game-header">
|
|||
|
|
<text class="game-header__title">{{ currentTabName }}</text>
|
|||
|
|
<text class="game-header__time">Day {{ gameTime.day }} {{ gameTimeText }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 内容区域 -->
|
|||
|
|
<view class="game-content">
|
|||
|
|
<StatusPanel v-show="game.currentTab === 'status'" />
|
|||
|
|
<MapPanel v-show="game.currentTab === 'map'" />
|
|||
|
|
<LogPanel v-show="game.currentTab === 'log'" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 底部导航 -->
|
|||
|
|
<TabBar
|
|||
|
|
:tabs="tabs"
|
|||
|
|
v-model="game.currentTab"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 背包抽屉 -->
|
|||
|
|
<Drawer
|
|||
|
|
:visible="game.drawerState.inventory"
|
|||
|
|
@close="game.drawerState.inventory = false"
|
|||
|
|
>
|
|||
|
|
<InventoryDrawer />
|
|||
|
|
</Drawer>
|
|||
|
|
|
|||
|
|
<!-- 事件抽屉 -->
|
|||
|
|
<Drawer
|
|||
|
|
:visible="game.drawerState.event"
|
|||
|
|
@close="game.drawerState.event = false"
|
|||
|
|
>
|
|||
|
|
<EventDrawer />
|
|||
|
|
</Drawer>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed } from 'vue'
|
|||
|
|
import { useGameStore } from '@/store/game'
|
|||
|
|
import TabBar from '@/components/layout/TabBar.vue'
|
|||
|
|
import Drawer from '@/components/layout/Drawer.vue'
|
|||
|
|
import StatusPanel from '@/components/panels/StatusPanel.vue'
|
|||
|
|
import MapPanel from '@/components/panels/MapPanel.vue'
|
|||
|
|
import LogPanel from '@/components/panels/LogPanel.vue'
|
|||
|
|
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
|
|||
|
|
import EventDrawer from '@/components/drawers/EventDrawer.vue'
|
|||
|
|
|
|||
|
|
const game = useGameStore()
|
|||
|
|
|
|||
|
|
const tabs = [
|
|||
|
|
{ id: 'status', label: '状态', icon: '👤' },
|
|||
|
|
{ id: 'map', label: '地图', icon: '🗺️' },
|
|||
|
|
{ id: 'log', label: '日志', icon: '📜' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const currentTabName = computed(() => {
|
|||
|
|
return tabs.find(t => t.id === game.currentTab)?.label || ''
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const gameTime = computed(() => game.gameTime)
|
|||
|
|
|
|||
|
|
const gameTimeText = computed(() => {
|
|||
|
|
const h = String(gameTime.value.hour).padStart(2, '0')
|
|||
|
|
const m = String(gameTime.value.minute).padStart(2, '0')
|
|||
|
|
return `${h}:${m}`
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.game-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100vh;
|
|||
|
|
background-color: $bg-primary;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 16rpx 24rpx;
|
|||
|
|
background-color: $bg-secondary;
|
|||
|
|
border-bottom: 1rpx solid $border-color;
|
|||
|
|
|
|||
|
|
&__title { font-size: 32rpx; font-weight: bold; }
|
|||
|
|
&__time { color: $text-secondary; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-content {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 三个Tab可正常切换
|
|||
|
|
- [ ] 状态面板显示基础属性
|
|||
|
|
- [ ] 地图面板显示区域列表
|
|||
|
|
- [ ] 日志面板显示日志消息
|
|||
|
|
- [ ] 顶部显示游戏时间
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `components/panels/StatusPanel.vue` | 创建 | 状态面板 |
|
|||
|
|
| `components/panels/MapPanel.vue` | 创建 | 地图面板 |
|
|||
|
|
| `components/panels/LogPanel.vue` | 创建 | 日志面板 |
|
|||
|
|
| `pages/index/index.vue` | 修改 | 主页面容器 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 5: 抽屉组件开发
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
开发背包抽屉和事件对话抽屉。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1-4 完成
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 5.1 创建背包抽屉组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\drawers\InventoryDrawer.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- FilterTabs 分类筛选
|
|||
|
|
- 物品列表显示
|
|||
|
|
- 物品详情弹窗
|
|||
|
|
- 使用/装备/出售操作
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="inventory-drawer">
|
|||
|
|
<view class="drawer-header">
|
|||
|
|
<text class="drawer-title">📦 背包</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<FilterTabs
|
|||
|
|
:tabs="filterTabs"
|
|||
|
|
v-model="currentFilter"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<scroll-view class="inventory-list" scroll-y>
|
|||
|
|
<view
|
|||
|
|
v-for="item in filteredItems"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="inventory-item"
|
|||
|
|
@click="selectItem(item)"
|
|||
|
|
>
|
|||
|
|
<text class="inventory-item__icon">{{ item.icon }}</text>
|
|||
|
|
<view class="inventory-item__info">
|
|||
|
|
<text class="inventory-item__name" :style="{ color: qualityColor(item.quality) }">
|
|||
|
|
{{ item.name }}
|
|||
|
|
</text>
|
|||
|
|
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 物品详情 -->
|
|||
|
|
<view v-if="selectedItem" class="item-detail">
|
|||
|
|
<text class="item-detail__name">{{ selectedItem.name }}</text>
|
|||
|
|
<text class="item-detail__desc">{{ selectedItem.description }}</text>
|
|||
|
|
<view class="item-detail__actions">
|
|||
|
|
<TextButton
|
|||
|
|
v-if="canEquip"
|
|||
|
|
text="装备"
|
|||
|
|
@click="equipItem"
|
|||
|
|
/>
|
|||
|
|
<TextButton
|
|||
|
|
v-if="canUse"
|
|||
|
|
text="使用"
|
|||
|
|
@click="useItem"
|
|||
|
|
/>
|
|||
|
|
<TextButton
|
|||
|
|
text="出售"
|
|||
|
|
@click="sellItem"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
import { usePlayerStore } from '@/store/player'
|
|||
|
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
|||
|
|
import TextButton from '@/components/common/TextButton.vue'
|
|||
|
|
|
|||
|
|
const player = usePlayerStore()
|
|||
|
|
const currentFilter = ref('all')
|
|||
|
|
const selectedItem = ref(null)
|
|||
|
|
|
|||
|
|
const filterTabs = [
|
|||
|
|
{ id: 'all', label: '全部' },
|
|||
|
|
{ id: 'weapon', label: '武器' },
|
|||
|
|
{ id: 'armor', label: '防具' },
|
|||
|
|
{ id: 'consumable', label: '消耗品' },
|
|||
|
|
{ id: 'material', label: '素材' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const filteredItems = computed(() => {
|
|||
|
|
// 根据筛选返回物品
|
|||
|
|
return player.inventory
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function qualityColor(quality) {
|
|||
|
|
// 返回品质对应颜色
|
|||
|
|
return '#ffffff'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectItem(item) {
|
|||
|
|
selectedItem.value = item
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function equipItem() {
|
|||
|
|
// 装备逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function useItem() {
|
|||
|
|
// 使用逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function sellItem() {
|
|||
|
|
// 出售逻辑
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.inventory-drawer {
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.drawer-header {
|
|||
|
|
padding: 24rpx;
|
|||
|
|
border-bottom: 1rpx solid $border-color;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.inventory-list {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 16rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.2 创建事件对话抽屉组件
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\components\drawers\EventDrawer.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- NPC信息区(名称、头像)
|
|||
|
|
- 对话文本区(支持滚动)
|
|||
|
|
- 选项按钮区
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="event-drawer">
|
|||
|
|
<view class="event-header">
|
|||
|
|
<text class="event-title">{{ eventData.title }}</text>
|
|||
|
|
<TextButton text="×" @click="close" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="event-content">
|
|||
|
|
<!-- NPC信息 -->
|
|||
|
|
<view v-if="eventData.npc" class="npc-info">
|
|||
|
|
<text class="npc-name">{{ eventData.npc.name }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 对话文本 -->
|
|||
|
|
<scroll-view class="dialogue-text" scroll-y>
|
|||
|
|
<text>{{ eventData.text }}</text>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 选项按钮 -->
|
|||
|
|
<view class="dialogue-choices">
|
|||
|
|
<TextButton
|
|||
|
|
v-for="choice in eventData.choices"
|
|||
|
|
:key="choice.id"
|
|||
|
|
:text="choice.text"
|
|||
|
|
@click="selectChoice(choice)"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { computed } from 'vue'
|
|||
|
|
import { useGameStore } from '@/store/game'
|
|||
|
|
import TextButton from '@/components/common/TextButton.vue'
|
|||
|
|
|
|||
|
|
const game = useGameStore()
|
|||
|
|
|
|||
|
|
const eventData = computed(() => {
|
|||
|
|
// 返回当前事件数据
|
|||
|
|
return {
|
|||
|
|
title: '事件',
|
|||
|
|
npc: null,
|
|||
|
|
text: '事件内容...',
|
|||
|
|
choices: []
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function close() {
|
|||
|
|
game.drawerState.event = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectChoice(choice) {
|
|||
|
|
// 处理选择
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.event-drawer {
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.event-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 24rpx;
|
|||
|
|
border-bottom: 1rpx solid $border-color;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.event-content {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dialogue-text {
|
|||
|
|
max-height: 400rpx;
|
|||
|
|
margin: 24rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dialogue-choices {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 地图面板「背包」按钮可打开抽屉
|
|||
|
|
- [ ] 抽屉动画流畅
|
|||
|
|
- [ ] 物品列表正常显示
|
|||
|
|
- [ ] 事件对话可显示
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `components/drawers/InventoryDrawer.vue` | 创建 | 背包抽屉 |
|
|||
|
|
| `components/drawers/EventDrawer.vue` | 创建 | 事件抽屉 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 6: 核心系统实现
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
实现技能系统、战斗系统、物品系统、任务系统、事件系统、环境系统的核心逻辑。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1-5 完成
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 6.1 技能系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\skillSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 经验添加
|
|||
|
|
- 升级判定
|
|||
|
|
- 里程碑检测
|
|||
|
|
- 父技能同步
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加技能经验
|
|||
|
|
* @param {Object} playerStore - 玩家Store
|
|||
|
|
* @param {String} skillId - 技能ID
|
|||
|
|
* @param {Number} amount - 经验值
|
|||
|
|
*/
|
|||
|
|
export function addSkillExp(playerStore, skillId, amount) {
|
|||
|
|
const skill = playerStore.skills[skillId]
|
|||
|
|
if (!skill || !skill.unlocked) return
|
|||
|
|
|
|||
|
|
skill.exp += amount
|
|||
|
|
|
|||
|
|
// 检查升级
|
|||
|
|
const config = SKILL_CONFIG[skillId]
|
|||
|
|
while (skill.level < config.maxLevel && skill.exp >= getNextLevelExp(skillId, skill.level)) {
|
|||
|
|
levelUpSkill(playerStore, skillId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取下一级所需经验
|
|||
|
|
*/
|
|||
|
|
export function getNextLevelExp(skillId, currentLevel) {
|
|||
|
|
const config = SKILL_CONFIG[skillId]
|
|||
|
|
return config.expPerLevel(currentLevel + 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 技能升级
|
|||
|
|
*/
|
|||
|
|
export function levelUpSkill(playerStore, skillId) {
|
|||
|
|
const skill = playerStore.skills[skillId]
|
|||
|
|
const config = SKILL_CONFIG[skillId]
|
|||
|
|
|
|||
|
|
skill.level++
|
|||
|
|
skill.exp -= config.expPerLevel(skill.level)
|
|||
|
|
|
|||
|
|
// 检查里程碑奖励
|
|||
|
|
if (config.milestones[skill.level]) {
|
|||
|
|
applyMilestoneBonus(playerStore, skillId, skill.level)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新父技能等级
|
|||
|
|
updateParentSkill(playerStore, skillId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用里程碑奖励
|
|||
|
|
*/
|
|||
|
|
export function applyMilestoneBonus(playerStore, skillId, level) {
|
|||
|
|
const config = SKILL_CONFIG[skillId]
|
|||
|
|
const milestone = config.milestones[level]
|
|||
|
|
if (!milestone) return
|
|||
|
|
|
|||
|
|
// 应用全局奖励
|
|||
|
|
// TODO: 实现奖励效果
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新父技能等级
|
|||
|
|
*/
|
|||
|
|
export function updateParentSkill(playerStore, skillId) {
|
|||
|
|
// TODO: 父技能逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解锁技能
|
|||
|
|
*/
|
|||
|
|
export function unlockSkill(playerStore, skillId) {
|
|||
|
|
if (!playerStore.skills[skillId]) {
|
|||
|
|
playerStore.skills[skillId] = {
|
|||
|
|
level: 0,
|
|||
|
|
exp: 0,
|
|||
|
|
unlocked: true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.2 战斗系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\combatSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- AP/EP命中计算
|
|||
|
|
- 三层防御(闪避/护盾/防御)
|
|||
|
|
- 战斗tick处理
|
|||
|
|
- 战斗日志输出
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
/**
|
|||
|
|
* 计算AP(攻击点数)
|
|||
|
|
* AP = 灵巧 + 智力*0.2 + 技能加成 + 装备加成 + 姿态加成 - 敌人数惩罚
|
|||
|
|
*/
|
|||
|
|
export function calculateAP(player, stance, enemyCount, environment) {
|
|||
|
|
let ap = player.baseStats.dexterity + player.baseStats.intuition * 0.2
|
|||
|
|
|
|||
|
|
// 姿态加成
|
|||
|
|
if (stance === 'attack') ap *= 1.1
|
|||
|
|
if (stance === 'defense') ap *= 0.9
|
|||
|
|
|
|||
|
|
// 环境惩罚
|
|||
|
|
if (environment === 'dark') ap *= 0.8
|
|||
|
|
|
|||
|
|
// 多敌人惩罚
|
|||
|
|
ap *= (1 - Math.min(0.3, (enemyCount - 1) * 0.05))
|
|||
|
|
|
|||
|
|
return Math.floor(ap)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算EP(闪避点数)
|
|||
|
|
* EP = 敏捷 + 智力*0.2 + 技能加成 + 装备加成 + 姿态加成 - 环境惩罚
|
|||
|
|
*/
|
|||
|
|
export function calculateEP(player, stance, environment) {
|
|||
|
|
let ep = player.baseStats.agility + player.baseStats.intuition * 0.2
|
|||
|
|
|
|||
|
|
// 姿态加成
|
|||
|
|
if (stance === 'defense') ep *= 1.2
|
|||
|
|
if (stance === 'attack') ep *= 0.8
|
|||
|
|
|
|||
|
|
// 环境惩罚
|
|||
|
|
if (environment === 'dark') ep *= 0.8
|
|||
|
|
|
|||
|
|
return Math.floor(ep)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算命中概率
|
|||
|
|
* @param {Number} ap - 攻击点数
|
|||
|
|
* @param {Number} ep - 闪避点数
|
|||
|
|
* @returns {Number} 命中概率 (0-1)
|
|||
|
|
*/
|
|||
|
|
export function calculateHitRate(ap, ep) {
|
|||
|
|
const ratio = ap / (ep + 1)
|
|||
|
|
// 非线性计算
|
|||
|
|
if (ratio >= 5) return 0.98
|
|||
|
|
if (ratio >= 2) return 0.8
|
|||
|
|
if (ratio >= 1) return 0.5
|
|||
|
|
if (ratio >= 0.5) return 0.24
|
|||
|
|
return 0.05
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理攻击
|
|||
|
|
* @returns {Object} { hit, damage, crit, evaded, blocked }
|
|||
|
|
*/
|
|||
|
|
export function processAttack(attacker, defender, stance, environment) {
|
|||
|
|
const ap = calculateAP(attacker, stance, 1, environment)
|
|||
|
|
const ep = calculateEP(defender, 'balance', environment)
|
|||
|
|
const hitRate = calculateHitRate(ap, ep)
|
|||
|
|
|
|||
|
|
// 闪避判定
|
|||
|
|
const roll = Math.random()
|
|||
|
|
if (roll > hitRate) {
|
|||
|
|
return { hit: false, evaded: true, damage: 0 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算基础伤害
|
|||
|
|
let damage = attacker.baseStats.attack || 0
|
|||
|
|
|
|||
|
|
// 暴击判定
|
|||
|
|
const critRate = 0.05 + (attacker.baseStats.dexterity / 100)
|
|||
|
|
const crit = Math.random() < critRate
|
|||
|
|
if (crit) {
|
|||
|
|
damage *= 1.5
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 防御力减免
|
|||
|
|
const defense = defender.baseStats.defense || 0
|
|||
|
|
damage = Math.max(1, damage - defense)
|
|||
|
|
damage = Math.max(damage * 0.1, damage) // 最小10%伤害
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
hit: true,
|
|||
|
|
damage: Math.floor(damage),
|
|||
|
|
crit,
|
|||
|
|
evaded: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理战斗tick
|
|||
|
|
*/
|
|||
|
|
export function combatTick(gameStore, playerStore, combatState) {
|
|||
|
|
// 玩家攻击
|
|||
|
|
const playerAttack = processAttack(
|
|||
|
|
playerStore,
|
|||
|
|
combatState.enemy,
|
|||
|
|
combatState.stance,
|
|||
|
|
combatState.environment
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if (playerAttack.hit) {
|
|||
|
|
combatState.enemy.hp -= playerAttack.damage
|
|||
|
|
const log = playerAttack.crit ? '暴击!' : ''
|
|||
|
|
gameStore.addLog(`你攻击${combatState.enemy.name},造成${playerAttack.damage}点伤害 ${log}`, 'combat')
|
|||
|
|
} else {
|
|||
|
|
gameStore.addLog(`你攻击${combatState.enemy.name},未命中`, 'combat')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查敌人是否死亡
|
|||
|
|
if (combatState.enemy.hp <= 0) {
|
|||
|
|
return { victory: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 敌人攻击
|
|||
|
|
const enemyAttack = processAttack(
|
|||
|
|
combatState.enemy,
|
|||
|
|
playerStore,
|
|||
|
|
'balance',
|
|||
|
|
combatState.environment
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if (enemyAttack.hit) {
|
|||
|
|
playerStore.currentStats.health -= enemyAttack.damage
|
|||
|
|
gameStore.addLog(`${combatState.enemy.name}攻击你,造成${enemyAttack.damage}点伤害`, 'combat')
|
|||
|
|
} else {
|
|||
|
|
gameStore.addLog(`${combatState.enemy.name}攻击你,你闪避了!`, 'combat')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查玩家是否死亡
|
|||
|
|
if (playerStore.currentStats.health <= 0) {
|
|||
|
|
return { defeat: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { continue: true }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.3 物品系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\itemSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 使用物品
|
|||
|
|
- 装备/卸下
|
|||
|
|
- 品质属性计算
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
import { ITEM_CONFIG } from '@/config/items.js'
|
|||
|
|
import { GAME_CONSTANTS } from '@/config/constants.js'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算装备品质后的属性
|
|||
|
|
*/
|
|||
|
|
export function calculateItemStats(itemId, quality = 100) {
|
|||
|
|
const config = ITEM_CONFIG[itemId]
|
|||
|
|
if (!config) return null
|
|||
|
|
|
|||
|
|
const qualityLevel = getQualityLevel(quality)
|
|||
|
|
const qualityMultiplier = qualityLevel.multiplier
|
|||
|
|
|
|||
|
|
const stats = { ...config }
|
|||
|
|
|
|||
|
|
// 应用品质百分比
|
|||
|
|
if (config.baseDamage) {
|
|||
|
|
stats.finalDamage = Math.floor(config.baseDamage * (quality / 100) * qualityMultiplier)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (config.baseDefense) {
|
|||
|
|
stats.finalDefense = Math.floor(config.baseDefense * (quality / 100) * qualityMultiplier)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算价值
|
|||
|
|
stats.finalValue = Math.floor(config.baseValue * (quality / 100) * qualityMultiplier)
|
|||
|
|
|
|||
|
|
stats.quality = quality
|
|||
|
|
stats.qualityLevel = qualityLevel
|
|||
|
|
|
|||
|
|
return stats
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取品质等级
|
|||
|
|
*/
|
|||
|
|
export function getQualityLevel(quality) {
|
|||
|
|
for (const [level, data] of Object.entries(GAME_CONSTANTS.QUALITY_LEVELS)) {
|
|||
|
|
if (quality >= data.range[0] && quality <= data.range[1]) {
|
|||
|
|
return { level: parseInt(level), ...data }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return GAME_CONSTANTS.QUALITY_LEVELS[1]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 使用物品
|
|||
|
|
*/
|
|||
|
|
export function useItem(playerStore, gameStore, itemId) {
|
|||
|
|
const config = ITEM_CONFIG[itemId]
|
|||
|
|
if (!config) return { success: false, message: '物品不存在' }
|
|||
|
|
|
|||
|
|
// 消耗品
|
|||
|
|
if (config.type === 'consumable') {
|
|||
|
|
const item = playerStore.inventory.find(i => i.id === itemId)
|
|||
|
|
if (!item || item.count <= 0) {
|
|||
|
|
return { success: false, message: '物品数量不足' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用效果
|
|||
|
|
if (config.effect.health) {
|
|||
|
|
playerStore.currentStats.health = Math.min(
|
|||
|
|
playerStore.currentStats.maxHealth,
|
|||
|
|
playerStore.currentStats.health + config.effect.health
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
if (config.effect.stamina) {
|
|||
|
|
playerStore.currentStats.stamina = Math.min(
|
|||
|
|
playerStore.currentStats.maxStamina,
|
|||
|
|
playerStore.currentStats.stamina + config.effect.stamina
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 消耗物品
|
|||
|
|
item.count--
|
|||
|
|
if (item.count <= 0) {
|
|||
|
|
playerStore.inventory = playerStore.inventory.filter(i => i.id !== itemId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameStore.addLog(`使用了 ${config.name}`, 'info')
|
|||
|
|
return { success: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { success: false, message: '该物品无法使用' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 装备物品
|
|||
|
|
*/
|
|||
|
|
export function equipItem(playerStore, gameStore, itemId) {
|
|||
|
|
const config = ITEM_CONFIG[itemId]
|
|||
|
|
if (!config) return { success: false }
|
|||
|
|
|
|||
|
|
const item = playerStore.inventory.find(i => i.id === itemId && i.equipped === false)
|
|||
|
|
if (!item) return { success: false, message: '物品不存在或已装备' }
|
|||
|
|
|
|||
|
|
// 卸下当前装备
|
|||
|
|
const slot = config.type === 'weapon' ? 'weapon' : config.type
|
|||
|
|
if (playerStore.equipment[slot]) {
|
|||
|
|
unequipItem(playerStore, gameStore, slot)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 装备新物品
|
|||
|
|
playerStore.equipment[slot] = item
|
|||
|
|
item.equipped = true
|
|||
|
|
|
|||
|
|
gameStore.addLog(`装备了 ${config.name}`, 'info')
|
|||
|
|
return { success: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 卸下装备
|
|||
|
|
*/
|
|||
|
|
export function unequipItem(playerStore, gameStore, slot) {
|
|||
|
|
const equipped = playerStore.equipment[slot]
|
|||
|
|
if (!equipped) return { success: false }
|
|||
|
|
|
|||
|
|
equipped.equipped = false
|
|||
|
|
playerStore.equipment[slot] = null
|
|||
|
|
|
|||
|
|
gameStore.addLog(`卸下了 ${equipped.name}`, 'info')
|
|||
|
|
return { success: true }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.4 任务系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\taskSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 挂机任务管理
|
|||
|
|
- 互斥检测
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
/**
|
|||
|
|
* 开始任务
|
|||
|
|
*/
|
|||
|
|
export function startTask(gameStore, playerStore, taskType, taskData) {
|
|||
|
|
// 检查互斥
|
|||
|
|
if (!canStartTask(gameStore, playerStore, taskType)) {
|
|||
|
|
return { success: false, message: '任务互斥' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const task = {
|
|||
|
|
id: Date.now(),
|
|||
|
|
type: taskType,
|
|||
|
|
data: taskData,
|
|||
|
|
startTime: Date.now()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameStore.activeTasks.push(task)
|
|||
|
|
return { success: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否可以开始任务
|
|||
|
|
*/
|
|||
|
|
export function canStartTask(gameStore, playerStore, taskType) {
|
|||
|
|
// 战斗互斥
|
|||
|
|
if (gameStore.inCombat && taskType !== 'combat') {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否已有相同类型任务
|
|||
|
|
const existingTask = gameStore.activeTasks.find(t => t.type === taskType)
|
|||
|
|
if (existingTask) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 结束任务
|
|||
|
|
*/
|
|||
|
|
export function endTask(gameStore, taskId) {
|
|||
|
|
gameStore.activeTasks = gameStore.activeTasks.filter(t => t.id !== taskId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理任务tick(每秒调用)
|
|||
|
|
*/
|
|||
|
|
export function processTaskTick(gameStore, playerStore) {
|
|||
|
|
for (const task of gameStore.activeTasks) {
|
|||
|
|
switch (task.type) {
|
|||
|
|
case 'reading':
|
|||
|
|
processReadingTask(gameStore, playerStore, task)
|
|||
|
|
break
|
|||
|
|
case 'resting':
|
|||
|
|
processRestingTask(gameStore, playerStore, task)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理阅读任务
|
|||
|
|
*/
|
|||
|
|
export function processReadingTask(gameStore, playerStore, task) {
|
|||
|
|
// 阅读获得经验
|
|||
|
|
// TODO: 实现
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理休息任务
|
|||
|
|
*/
|
|||
|
|
export function processRestingTask(gameStore, playerStore, task) {
|
|||
|
|
// 恢复耐力
|
|||
|
|
if (playerStore.currentStats.stamina < playerStore.currentStats.maxStamina) {
|
|||
|
|
playerStore.currentStats.stamina = Math.min(
|
|||
|
|
playerStore.currentStats.maxStamina,
|
|||
|
|
playerStore.currentStats.stamina + 5
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.5 事件系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\eventSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 事件触发检测
|
|||
|
|
- 对话管理
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
import { NPC_CONFIG } from '@/config/npcs.js'
|
|||
|
|
import { EVENT_CONFIG } from '@/config/events.js'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查并触发事件
|
|||
|
|
*/
|
|||
|
|
export function checkAndTriggerEvent(gameStore, playerStore) {
|
|||
|
|
// 检查首次进入事件
|
|||
|
|
if (!playerStore.flags?.introTriggered) {
|
|||
|
|
triggerEvent(gameStore, 'intro')
|
|||
|
|
playerStore.flags = playerStore.flags || {}
|
|||
|
|
playerStore.flags.introTriggered = true
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查位置事件
|
|||
|
|
// TODO: 实现
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触发事件
|
|||
|
|
*/
|
|||
|
|
export function triggerEvent(gameStore, eventId) {
|
|||
|
|
const config = EVENT_CONFIG[eventId]
|
|||
|
|
if (!config) return
|
|||
|
|
|
|||
|
|
gameStore.currentEvent = {
|
|||
|
|
id: eventId,
|
|||
|
|
...config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameStore.drawerState.event = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理NPC对话
|
|||
|
|
*/
|
|||
|
|
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
|||
|
|
const npc = NPC_CONFIG[npcId]
|
|||
|
|
if (!npc) return
|
|||
|
|
|
|||
|
|
const dialogue = npc.dialogue[dialogueKey]
|
|||
|
|
if (!dialogue) return
|
|||
|
|
|
|||
|
|
gameStore.currentEvent = {
|
|||
|
|
type: 'dialogue',
|
|||
|
|
npc: npc,
|
|||
|
|
text: dialogue.text,
|
|||
|
|
choices: dialogue.choices,
|
|||
|
|
currentDialogue: dialogueKey
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameStore.drawerState.event = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理对话选择
|
|||
|
|
*/
|
|||
|
|
export function handleDialogueChoice(gameStore, playerStore, choice) {
|
|||
|
|
if (choice.action) {
|
|||
|
|
processDialogueAction(gameStore, playerStore, choice.action)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (choice.next === null) {
|
|||
|
|
gameStore.drawerState.event = false
|
|||
|
|
} else if (choice.next) {
|
|||
|
|
startNPCDialogue(gameStore, gameStore.currentEvent.npc.id, choice.next)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理对话动作
|
|||
|
|
*/
|
|||
|
|
export function processDialogueAction(gameStore, playerStore, action) {
|
|||
|
|
switch (action) {
|
|||
|
|
case 'give_stick':
|
|||
|
|
// 给予木棍
|
|||
|
|
// TODO: 实现
|
|||
|
|
break
|
|||
|
|
case 'open_shop':
|
|||
|
|
// 打开商店
|
|||
|
|
gameStore.drawerState.event = false
|
|||
|
|
// TODO: 打开商店抽屉
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6.6 环境系统工具
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\environmentSystem.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 环境惩罚计算
|
|||
|
|
- 被动技能获取
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
/**
|
|||
|
|
* 获取当前环境惩罚
|
|||
|
|
*/
|
|||
|
|
export function getEnvironmentPenalty(locationId, playerStore) {
|
|||
|
|
// TODO: 从配置读取
|
|||
|
|
const penalties = {
|
|||
|
|
basement: { ap: 0.8, ep: 0.8, reading: 0.5 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return penalties[locationId] || { ap: 1, ep: 1, reading: 1 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理环境被动经验
|
|||
|
|
*/
|
|||
|
|
export function processEnvironmentExp(gameStore, playerStore, locationId) {
|
|||
|
|
// 黑暗区域获得夜视经验
|
|||
|
|
if (locationId === 'basement') {
|
|||
|
|
// 检查是否已解锁夜视
|
|||
|
|
if (playerStore.skills.night_vision?.unlocked) {
|
|||
|
|
// 添加夜视经验
|
|||
|
|
// TODO: 调用技能系统
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 技能可以添加经验并升级
|
|||
|
|
- [ ] 战斗可以计算命中/闪避
|
|||
|
|
- [ ] 物品可以使用和装备
|
|||
|
|
- [ ] 任务可以开始和结束
|
|||
|
|
- [ ] 事件可以触发和选择
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `utils/skillSystem.js` | 创建 | 技能系统 |
|
|||
|
|
| `utils/combatSystem.js` | 创建 | 战斗系统 |
|
|||
|
|
| `utils/itemSystem.js` | 创建 | 物品系统 |
|
|||
|
|
| `utils/taskSystem.js` | 创建 | 任务系统 |
|
|||
|
|
| `utils/eventSystem.js` | 创建 | 事件系统 |
|
|||
|
|
| `utils/environmentSystem.js` | 创建 | 环境系统 |
|
|||
|
|
| `config/events.js` | 创建 | 事件配置 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 7: 游戏循环和存档系统
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
实现游戏主循环、数据持久化、离线收益计算。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1-6 完成
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 7.1 游戏主循环
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\gameLoop.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- 每秒tick
|
|||
|
|
- 时间推进
|
|||
|
|
- 战斗处理
|
|||
|
|
- 任务经验
|
|||
|
|
- 耐力恢复
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
import { GAME_CONSTANTS } from '@/config/constants.js'
|
|||
|
|
import { combatTick } from './combatSystem.js'
|
|||
|
|
import { processTaskTick } from './taskSystem.js'
|
|||
|
|
import { processEnvironmentExp } from './environmentSystem.js'
|
|||
|
|
|
|||
|
|
let gameTimer = null
|
|||
|
|
let lastTickTime = 0
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 启动游戏循环
|
|||
|
|
*/
|
|||
|
|
export function startGameLoop(gameStore, playerStore) {
|
|||
|
|
if (gameTimer) {
|
|||
|
|
stopGameLoop()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastTickTime = Date.now()
|
|||
|
|
|
|||
|
|
gameTimer = setInterval(() => {
|
|||
|
|
gameTick(gameStore, playerStore)
|
|||
|
|
}, 1000) // 每秒执行一次
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止游戏循环
|
|||
|
|
*/
|
|||
|
|
export function stopGameLoop() {
|
|||
|
|
if (gameTimer) {
|
|||
|
|
clearInterval(gameTimer)
|
|||
|
|
gameTimer = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 游戏主tick
|
|||
|
|
*/
|
|||
|
|
export function gameTick(gameStore, playerStore) {
|
|||
|
|
// 更新游戏时间
|
|||
|
|
updateGameTime(gameStore)
|
|||
|
|
|
|||
|
|
// 处理战斗
|
|||
|
|
if (gameStore.inCombat) {
|
|||
|
|
const result = combatTick(gameStore, playerStore, gameStore.combatState)
|
|||
|
|
|
|||
|
|
if (result.victory) {
|
|||
|
|
handleCombatVictory(gameStore, playerStore)
|
|||
|
|
} else if (result.defeat) {
|
|||
|
|
handleCombatDefeat(gameStore, playerStore)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理任务
|
|||
|
|
processTaskTick(gameStore, playerStore)
|
|||
|
|
|
|||
|
|
// 处理环境被动经验
|
|||
|
|
processEnvironmentExp(gameStore, playerStore, playerStore.currentLocation)
|
|||
|
|
|
|||
|
|
// 自动保存(每30秒)
|
|||
|
|
if (gameStore.gameTime.totalMinutes % 30 === 0) {
|
|||
|
|
saveGame(gameStore, playerStore)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新游戏时间
|
|||
|
|
* 现实1秒 = 游戏时间5分钟
|
|||
|
|
*/
|
|||
|
|
export function updateGameTime(gameStore) {
|
|||
|
|
const time = gameStore.gameTime
|
|||
|
|
time.totalMinutes += GAME_CONSTANTS.TIME_SCALE
|
|||
|
|
|
|||
|
|
// 计算天时分
|
|||
|
|
const totalDayMinutes = time.totalMinutes % (24 * 60) // 一天内的分钟数
|
|||
|
|
time.day = Math.floor(time.totalMinutes / (24 * 60)) + 1
|
|||
|
|
time.hour = Math.floor(totalDayMinutes / 60)
|
|||
|
|
time.minute = totalDayMinutes % 60
|
|||
|
|
|
|||
|
|
// 刷新市场价格(每天0点)
|
|||
|
|
if (time.hour === 0 && time.minute === 0) {
|
|||
|
|
refreshMarketPrices(gameStore)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理战斗胜利
|
|||
|
|
*/
|
|||
|
|
export function handleCombatVictory(gameStore, playerStore) {
|
|||
|
|
const combat = gameStore.combatState
|
|||
|
|
const enemy = combat.enemy
|
|||
|
|
|
|||
|
|
gameStore.addLog(`${enemy.name} 被击败了!`, 'system')
|
|||
|
|
|
|||
|
|
// 获得经验
|
|||
|
|
playerStore.level.exp += enemy.expReward
|
|||
|
|
|
|||
|
|
// 处理掉落
|
|||
|
|
for (const drop of enemy.drops) {
|
|||
|
|
if (Math.random() < drop.chance) {
|
|||
|
|
if (drop.item === 'copper') {
|
|||
|
|
const amount = Math.floor(Math.random() * (drop.max - drop.min + 1)) + drop.min
|
|||
|
|
playerStore.currency.copper += amount
|
|||
|
|
gameStore.addLog(`获得 ${amount} 铜币`, 'reward')
|
|||
|
|
} else {
|
|||
|
|
// 添加物品到背包
|
|||
|
|
// TODO: 实现
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 结束战斗
|
|||
|
|
gameStore.inCombat = false
|
|||
|
|
gameStore.combatState = null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理战斗失败
|
|||
|
|
*/
|
|||
|
|
export function handleCombatDefeat(gameStore, playerStore) {
|
|||
|
|
gameStore.addLog('你被击败了...', 'danger')
|
|||
|
|
|
|||
|
|
// 返回营地
|
|||
|
|
playerStore.currentLocation = 'camp'
|
|||
|
|
playerStore.currentStats.health = Math.floor(playerStore.currentStats.maxHealth * 0.5)
|
|||
|
|
|
|||
|
|
gameStore.inCombat = false
|
|||
|
|
gameStore.combatState = null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 刷新市场价格
|
|||
|
|
*/
|
|||
|
|
export function refreshMarketPrices(gameStore) {
|
|||
|
|
gameStore.marketPrices.lastRefreshDay = gameStore.gameTime.day
|
|||
|
|
// TODO: 实现价格随机波动
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.2 存储系统
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\utils\storage.js`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- saveGame()
|
|||
|
|
- loadGame()
|
|||
|
|
- resetGame()
|
|||
|
|
- logs[]不持久化
|
|||
|
|
|
|||
|
|
**代码结构**:
|
|||
|
|
```javascript
|
|||
|
|
const SAVE_KEY = 'game_save_v1'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存游戏
|
|||
|
|
*/
|
|||
|
|
export function saveGame(gameStore, playerStore) {
|
|||
|
|
try {
|
|||
|
|
const saveData = {
|
|||
|
|
version: 1,
|
|||
|
|
timestamp: Date.now(),
|
|||
|
|
|
|||
|
|
// 玩家数据
|
|||
|
|
player: {
|
|||
|
|
baseStats: playerStore.baseStats,
|
|||
|
|
currentStats: playerStore.currentStats,
|
|||
|
|
level: playerStore.level,
|
|||
|
|
skills: playerStore.skills,
|
|||
|
|
equipment: playerStore.equipment,
|
|||
|
|
inventory: playerStore.inventory,
|
|||
|
|
currency: playerStore.currency,
|
|||
|
|
currentLocation: playerStore.currentLocation
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 游戏数据
|
|||
|
|
game: {
|
|||
|
|
gameTime: gameStore.gameTime,
|
|||
|
|
marketPrices: gameStore.marketPrices,
|
|||
|
|
// 不保存logs和临时状态
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.setStorageSync(SAVE_KEY, JSON.stringify(saveData))
|
|||
|
|
return { success: true }
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('保存失败', e)
|
|||
|
|
return { success: false, error: e.message }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加载游戏
|
|||
|
|
*/
|
|||
|
|
export function loadGame() {
|
|||
|
|
try {
|
|||
|
|
const saveDataStr = uni.getStorageSync(SAVE_KEY)
|
|||
|
|
if (!saveDataStr) {
|
|||
|
|
return { success: false, exists: false }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const saveData = JSON.parse(saveDataStr)
|
|||
|
|
|
|||
|
|
// 检查版本兼容性
|
|||
|
|
if (saveData.version !== 1) {
|
|||
|
|
return { success: false, error: '存档版本不兼容' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { success: true, data: saveData }
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载失败', e)
|
|||
|
|
return { success: false, error: e.message }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置游戏
|
|||
|
|
*/
|
|||
|
|
export function resetGame(gameStore, playerStore) {
|
|||
|
|
uni.removeStorageSync(SAVE_KEY)
|
|||
|
|
|
|||
|
|
// 重置Store
|
|||
|
|
playerStore.resetPlayer()
|
|||
|
|
gameStore.resetGame()
|
|||
|
|
|
|||
|
|
return { success: true }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用存档数据到Store
|
|||
|
|
*/
|
|||
|
|
export function applySaveData(gameStore, playerStore, saveData) {
|
|||
|
|
// 应用玩家数据
|
|||
|
|
Object.assign(playerStore.baseStats, saveData.player.baseStats)
|
|||
|
|
Object.assign(playerStore.currentStats, saveData.player.currentStats)
|
|||
|
|
Object.assign(playerStore.level, saveData.player.level)
|
|||
|
|
playerStore.skills = saveData.player.skills
|
|||
|
|
playerStore.equipment = saveData.player.equipment
|
|||
|
|
playerStore.inventory = saveData.player.inventory
|
|||
|
|
playerStore.currency = saveData.player.currency
|
|||
|
|
playerStore.currentLocation = saveData.player.currentLocation
|
|||
|
|
|
|||
|
|
// 应用游戏数据
|
|||
|
|
gameStore.gameTime = saveData.game.gameTime
|
|||
|
|
gameStore.marketPrices = saveData.game.marketPrices
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.3 修改App.vue
|
|||
|
|
**文件路径**:`d:\uniapp\app_test\wwa3\App.vue`
|
|||
|
|
|
|||
|
|
**功能要点**:
|
|||
|
|
- onLaunch: 加载存档 + 启动循环
|
|||
|
|
- onHide: 自动保存
|
|||
|
|
|
|||
|
|
**代码修改**:
|
|||
|
|
```vue
|
|||
|
|
<script setup>
|
|||
|
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
|||
|
|
import { usePlayerStore } from '@/store/player'
|
|||
|
|
import { useGameStore } from '@/store/game'
|
|||
|
|
import { loadGame, saveGame, resetGame, applySaveData } from '@/utils/storage'
|
|||
|
|
import { startGameLoop, stopGameLoop } from '@/utils/gameLoop'
|
|||
|
|
import { checkAndTriggerEvent } from '@/utils/eventSystem'
|
|||
|
|
|
|||
|
|
const playerStore = usePlayerStore()
|
|||
|
|
const gameStore = useGameStore()
|
|||
|
|
|
|||
|
|
onLaunch(() => {
|
|||
|
|
console.log('App Launch')
|
|||
|
|
|
|||
|
|
// 尝试加载存档
|
|||
|
|
const loadResult = loadGame()
|
|||
|
|
|
|||
|
|
if (loadResult.success && loadResult.exists) {
|
|||
|
|
applySaveData(gameStore, playerStore, loadResult.data)
|
|||
|
|
gameStore.addLog('欢迎回来!', 'system')
|
|||
|
|
} else {
|
|||
|
|
// 新游戏
|
|||
|
|
initNewGame()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 启动游戏循环
|
|||
|
|
startGameLoop(gameStore, playerStore)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onShow(() => {
|
|||
|
|
console.log('App Show')
|
|||
|
|
|
|||
|
|
// 检查离线收益
|
|||
|
|
checkOfflineReward()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onHide(() => {
|
|||
|
|
console.log('App Hide')
|
|||
|
|
|
|||
|
|
// 自动保存
|
|||
|
|
saveGame(gameStore, playerStore)
|
|||
|
|
|
|||
|
|
// 停止游戏循环
|
|||
|
|
stopGameLoop()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function initNewGame() {
|
|||
|
|
gameStore.addLog('欢迎来到文字冒险游戏!', 'system')
|
|||
|
|
checkAndTriggerEvent(gameStore, playerStore)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function checkOfflineReward() {
|
|||
|
|
// 计算离线时间
|
|||
|
|
const lastSave = localStorage.getItem('lastSaveTime')
|
|||
|
|
// TODO: 实现离线收益计算
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 游戏时间正常流逝
|
|||
|
|
- [ ] 关闭后重开数据保留
|
|||
|
|
- [ ] 战斗可自动进行
|
|||
|
|
- [ ] 存档可以正常读取和保存
|
|||
|
|
|
|||
|
|
### 预估文件列表
|
|||
|
|
| 文件路径 | 操作 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| `utils/gameLoop.js` | 创建 | 游戏主循环 |
|
|||
|
|
| `utils/storage.js` | 创建 | 存储系统 |
|
|||
|
|
| `App.vue` | 修改 | 集成存档和循环 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 8: 测试验证
|
|||
|
|
|
|||
|
|
### 目标描述
|
|||
|
|
按测试清单验证所有功能,确保数值平衡性。
|
|||
|
|
|
|||
|
|
### 前置依赖
|
|||
|
|
- Phase 1-7 完成
|
|||
|
|
|
|||
|
|
### 详细任务清单
|
|||
|
|
|
|||
|
|
#### 8.1 新游戏流程测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 清除存档(uni.removeStorageSync)
|
|||
|
|
2. 重新启动应用
|
|||
|
|
3. 验证初始剧情触发
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 初始属性正确(力量10、敏捷8等)
|
|||
|
|
- [ ] 货币为0
|
|||
|
|
- [ ] EventDrawer自动弹出,显示初始对话
|
|||
|
|
- [ ] 对话完成后获得木棍
|
|||
|
|
|
|||
|
|
#### 8.2 交易系统测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 前往测试市场
|
|||
|
|
2. 查看商品列表
|
|||
|
|
3. 购买面包
|
|||
|
|
4. 出售素材
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 商人售价约为200%基础价格
|
|||
|
|
- [ ] 商人收购价约为30%基础价格
|
|||
|
|
- [ ] 购买后货币正确扣除
|
|||
|
|
- [ ] 物品正确加入背包
|
|||
|
|
|
|||
|
|
#### 8.3 战斗系统测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 前往测试野外1
|
|||
|
|
2. 触发战斗
|
|||
|
|
3. 观察AP/EP计算
|
|||
|
|
4. 切换战斗姿态
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 战斗自动进行
|
|||
|
|
- [ ] 命中概率符合AP/EP比例
|
|||
|
|
- [ ] 姿态切换影响战斗
|
|||
|
|
- [ ] 击败敌人获得经验和物品
|
|||
|
|
- [ ] HP、耐力正确消耗
|
|||
|
|
|
|||
|
|
#### 8.4 技能系统测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 进行战斗获取武器技能经验
|
|||
|
|
2. 阅读书籍获取阅读经验
|
|||
|
|
3. 升级技能
|
|||
|
|
4. 检查里程碑奖励
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 技能经验正常积累
|
|||
|
|
- [ ] 技能可以升级
|
|||
|
|
- [ ] 里程碑奖励正确应用
|
|||
|
|
- [ ] 技能上限受玩家等级限制
|
|||
|
|
|
|||
|
|
#### 8.5 环境系统测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 解锁地下室
|
|||
|
|
2. 进入地下室
|
|||
|
|
3. 检查黑暗惩罚
|
|||
|
|
4. 观察夜视经验获取
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 黑暗区域AP/EP降低20%
|
|||
|
|
- [ ] 阅读效率降低50%
|
|||
|
|
- [ ] 夜视技能被动获得经验
|
|||
|
|
- [ ] 夜视技能升级后惩罚减少
|
|||
|
|
|
|||
|
|
#### 8.6 Boss战测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 击杀5只野狗
|
|||
|
|
2. 解锁Boss巢
|
|||
|
|
3. 挑战测试Boss
|
|||
|
|
4. 击败Boss
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 击杀5只野狗后解锁Boss巢
|
|||
|
|
- [ ] Boss战难度较高
|
|||
|
|
- [ ] 击败Boss掉落地下室钥匙
|
|||
|
|
- [ ] 解锁地下室区域
|
|||
|
|
|
|||
|
|
#### 8.7 存档系统测试
|
|||
|
|
|
|||
|
|
**测试步骤**:
|
|||
|
|
1. 进行游戏操作(获得物品、提升技能)
|
|||
|
|
2. 关闭应用
|
|||
|
|
3. 重新打开
|
|||
|
|
|
|||
|
|
**预期结果**:
|
|||
|
|
- [ ] 数据正确保留
|
|||
|
|
- [ ] 货币数量一致
|
|||
|
|
- [ ] 技能等级一致
|
|||
|
|
- [ ] 物品数量一致
|
|||
|
|
|
|||
|
|
#### 8.8 数值平衡测试
|
|||
|
|
|
|||
|
|
**测试重点**:
|
|||
|
|
|
|||
|
|
**AP/EP计算验证**:
|
|||
|
|
| 场景 | 预期AP | 预期EP | 预期命中率 |
|
|||
|
|
|------|--------|--------|------------|
|
|||
|
|
| 初始属性 vs 野狗 | ~10 | ~8 | ~50% |
|
|||
|
|
| 攻击姿态 | +10% | -20% | ~60% |
|
|||
|
|
| 防御姿态 | -10% | +20% | ~40% |
|
|||
|
|
| 黑暗环境 | -20% | -20% | ~50%(同比例) |
|
|||
|
|
|
|||
|
|
**技能升级速度验证**:
|
|||
|
|
| 技能 | 升到Lv.1 | 升到Lv.5 | 升到Lv.10 |
|
|||
|
|
|------|----------|----------|-----------|
|
|||
|
|
| 木棍精通 | ~10次攻击 | ~150次攻击 | ~600次攻击 |
|
|||
|
|
| 阅读 | 1本书 | 5本书 | 15本书 |
|
|||
|
|
|
|||
|
|
**经济平衡验证**:
|
|||
|
|
| 场景 | 预期收益 | 预期耗时 |
|
|||
|
|
|------|----------|----------|
|
|||
|
|
| 击杀野狗 | 5-15铜币 | ~10秒 |
|
|||
|
|
| 出售狗皮 | 3.5铜币(70%) | - |
|
|||
|
|
| 购买面包 | -20铜币 | - |
|
|||
|
|
|
|||
|
|
### 验收标准
|
|||
|
|
- [ ] 所有测试项通过
|
|||
|
|
- [ ] 无严重BUG
|
|||
|
|
- [ ] 数值基本平衡
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录:文件清单总览
|
|||
|
|
|
|||
|
|
### 目录结构
|
|||
|
|
```
|
|||
|
|
d:\uniapp\app_test\wwa3\
|
|||
|
|
├── components/
|
|||
|
|
│ <20><>── common/
|
|||
|
|
│ │ ├── ProgressBar.vue # 进度条
|
|||
|
|
│ │ ├── StatItem.vue # 属性显示
|
|||
|
|
│ │ ├── TextButton.vue # 文字按钮
|
|||
|
|
│ │ ├── Collapse.vue # 折叠面板
|
|||
|
|
│ │ └── FilterTabs.vue # 筛选标签
|
|||
|
|
│ ├── layout/
|
|||
|
|
│ │ ├── TabBar.vue # 底部导航
|
|||
|
|
│ │ └── Drawer.vue # 抽屉容器
|
|||
|
|
│ ├── panels/
|
|||
|
|
│ │ ├── StatusPanel.vue # 状态面板
|
|||
|
|
│ │ ├── MapPanel.vue # 地图面板
|
|||
|
|
│ │ └── LogPanel.vue # 日志面板
|
|||
|
|
│ └── drawers/
|
|||
|
|
│ ├── InventoryDrawer.vue # 背包抽屉
|
|||
|
|
│ └── EventDrawer.vue # 事件抽屉
|
|||
|
|
├── store/
|
|||
|
|
│ ├── player.js # 玩家状态
|
|||
|
|
│ └── game.js # 游戏状态
|
|||
|
|
├── config/
|
|||
|
|
│ ├── constants.js # 游戏常量
|
|||
|
|
│ ├── skills.js # 技能配置
|
|||
|
|
│ ├── items.js # 物品配置
|
|||
|
|
│ ├── locations.js # 区域配置
|
|||
|
|
│ ├── enemies.js # 敌人配置
|
|||
|
|
│ ├── npcs.js # NPC配置
|
|||
|
|
│ └── events.js # 事件配置
|
|||
|
|
├── utils/
|
|||
|
|
│ ├── skillSystem.js # 技能系统
|
|||
|
|
│ ├── combatSystem.js # 战斗系统
|
|||
|
|
│ ├── itemSystem.js # 物品系统
|
|||
|
|
│ ├── taskSystem.js # 任务系统
|
|||
|
|
│ ├── eventSystem.js # 事件系统
|
|||
|
|
│ ├── environmentSystem.js # 环境系统
|
|||
|
|
│ ├── gameLoop.js # 游戏循环
|
|||
|
|
│ └── storage.js # 存储系统
|
|||
|
|
├── pages/
|
|||
|
|
│ └── index/
|
|||
|
|
│ └── index.vue # 主页面
|
|||
|
|
├── App.vue # 应用入口
|
|||
|
|
├── main.js # 主入口
|
|||
|
|
├── uni.scss # 全局样式
|
|||
|
|
└── package.json # 依赖配置
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 创建文件总计
|
|||
|
|
| 类别 | 文件数量 |
|
|||
|
|
|------|----------|
|
|||
|
|
| 组件 | 12 |
|
|||
|
|
| Store | 2 |
|
|||
|
|
| 配置 | 7 |
|
|||
|
|
| 工具 | 8 |
|
|||
|
|
| 页面 | 1 |
|
|||
|
|
| 其他 | 3 |
|
|||
|
|
| **总计** | **33** |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 开发时间估算
|
|||
|
|
|
|||
|
|
| Phase | 预估时间 | 说明 |
|
|||
|
|
|-------|----------|------|
|
|||
|
|
| Phase 1 | 2小时 | 基础配置 |
|
|||
|
|
| Phase 2 | 3小时 | Store + 配置 |
|
|||
|
|
| Phase 3 | 4小时 | 7个组件 |
|
|||
|
|
| Phase 4 | 4小时 | 3个面板 + 主页面 |
|
|||
|
|
| Phase 5 | 2小时 | 2个抽屉 |
|
|||
|
|
| Phase 6 | 6小时 | 核心系统 |
|
|||
|
|
| Phase 7 | 3小时 | 循环 + 存储 |
|
|||
|
|
| Phase 8 | 4小时 | 测试修复 |
|
|||
|
|
| **总计** | **28小时** | 约3.5个工作日 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 注意事项
|
|||
|
|
|
|||
|
|
### 开发规范
|
|||
|
|
1. 所有组件使用 `<script setup>` 语法
|
|||
|
|
2. 样式使用 SCSS 和全局变量
|
|||
|
|
3. 状态管理统一使用 Pinia
|
|||
|
|
4. 定时器必须清理防止内存泄漏
|
|||
|
|
|
|||
|
|
### 数据验证
|
|||
|
|
1. 物品使用前检查数量
|
|||
|
|
2. 技能升级前检查上限
|
|||
|
|
3. 战斗前检查耐力和生命值
|
|||
|
|
4. 保存前验证数据格式
|
|||
|
|
|
|||
|
|
### 性能优化
|
|||
|
|
1. 日志限制在200条以内
|
|||
|
|
2. 计算属性使用缓存
|
|||
|
|
3. scroll-view 使用虚拟列表(长列表)
|
|||
|
|
4. 抽屉关闭时销毁内容
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
> 文档版本: v1.0
|
|||
|
|
> 创建日期: 2026-01-21
|
|||
|
|
> 基于设计文档: v2.0
|