Files
text-adventure-game/开发步骤文档.md
Claude cb412544e9 Initial commit: Text Adventure Game
Features:
- Combat system with AP/EP hit calculation and three-layer defense
- Auto-combat/farming mode
- Item system with stacking support
- Skill system with levels, milestones, and parent skill sync
- Shop system with dynamic pricing
- Inventory management with bulk selling
- Event system
- Game loop with offline earnings
- Save/Load system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:13:51 +08:00

3101 lines
72 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发步骤文档
> 基于设计文档 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