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>
72 KiB
开发步骤文档
基于设计文档 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/ # 类型定义(可选)
操作步骤:
- 在项目根目录执行:
mkdir -p components/common components/layout components/panels components/drawers mkdir -p store config utils types
1.2 安装 Pinia
npm install pinia
预期输出:package.json 中增加 "pinia": "^2.x.x"
1.3 修改 main.js 引入 Pinia
文件路径:d:\uniapp\app_test\wwa3\main.js
代码要点:
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
代码要点:
/* 背景色 */
$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
代码要点:
<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
代码要点:
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
代码要点:
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
// 游戏数值常量
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
// 技能配置
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
// 物品配置
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
// 区域配置
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
// 敌人配置
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
// 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' | 进度条高度 |
代码要点:
<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' | 文字颜色 |
代码要点:
<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 | 是否禁用 |
代码要点:
<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 | 是否展开 |
代码要点:
<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 |
代码要点:
<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 |
代码要点:
<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' | 抽屉宽度 |
代码要点:
<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
代码结构:
<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
- 活动按钮组
- 背包按钮唤起抽屉
代码结构:
<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显示日志
- 自动滚动到底部
- 不同类型日志的样式区分
代码结构:
<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 集成
代码结构:
<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 分类筛选
- 物品列表显示
- 物品详情弹窗
- 使用/装备/出售操作
代码结构:
<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信息区(名称、头像)
- 对话文本区(支持滚动)
- 选项按钮区
代码结构:
<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
功能要点:
- 经验添加
- 升级判定
- 里程碑检测
- 父技能同步
代码结构:
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处理
- 战斗日志输出
代码结构:
/**
* 计算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
功能要点:
- 使用物品
- 装备/卸下
- 品质属性计算
代码结构:
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
功能要点:
- 挂机任务管理
- 互斥检测
代码结构:
/**
* 开始任务
*/
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
功能要点:
- 事件触发检测
- 对话管理
代码结构:
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
功能要点:
- 环境惩罚计算
- 被动技能获取
代码结构:
/**
* 获取当前环境惩罚
*/
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
- 时间推进
- 战斗处理
- 任务经验
- 耐力恢复
代码结构:
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[]不持久化
代码结构:
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: 自动保存
代码修改:
<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 新游戏流程测试
测试步骤:
- 清除存档(uni.removeStorageSync)
- 重新启动应用
- 验证初始剧情触发
预期结果:
- 初始属性正确(力量10、敏捷8等)
- 货币为0
- EventDrawer自动弹出,显示初始对话
- 对话完成后获得木棍
8.2 交易系统测试
测试步骤:
- 前往测试市场
- 查看商品列表
- 购买面包
- 出售素材
预期结果:
- 商人售价约为200%基础价格
- 商人收购价约为30%基础价格
- 购买后货币正确扣除
- 物品正确加入背包
8.3 战斗系统测试
测试步骤:
- 前往测试野外1
- 触发战斗
- 观察AP/EP计算
- 切换战斗姿态
预期结果:
- 战斗自动进行
- 命中概率符合AP/EP比例
- 姿态切换影响战斗
- 击败敌人获得经验和物品
- HP、耐力正确消耗
8.4 技能系统测试
测试步骤:
- 进行战斗获取武器技能经验
- 阅读书籍获取阅读经验
- 升级技能
- 检查里程碑奖励
预期结果:
- 技能经验正常积累
- 技能可以升级
- 里程碑奖励正确应用
- 技能上限受玩家等级限制
8.5 环境系统测试
测试步骤:
- 解锁地下室
- 进入地下室
- 检查黑暗惩罚
- 观察夜视经验获取
预期结果:
- 黑暗区域AP/EP降低20%
- 阅读效率降低50%
- 夜视技能被动获得经验
- 夜视技能升级后惩罚减少
8.6 Boss战测试
测试步骤:
- 击杀5只野狗
- 解锁Boss巢
- 挑战测试Boss
- 击败Boss
预期结果:
- 击杀5只野狗后解锁Boss巢
- Boss战难度较高
- 击败Boss掉落地下室钥匙
- 解锁地下室区域
8.7 存档系统测试
测试步骤:
- 进行游戏操作(获得物品、提升技能)
- 关闭应用
- 重新打开
预期结果:
- 数据正确保留
- 货币数量一致
- 技能等级一致
- 物品数量一致
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个工作日 |
注意事项
开发规范
- 所有组件使用
<script setup>语法 - 样式使用 SCSS 和全局变量
- 状态管理统一使用 Pinia
- 定时器必须清理防止内存泄漏
数据验证
- 物品使用前检查数量
- 技能升级前检查上限
- 战斗前检查耐力和生命值
- 保存前验证数据格式
性能优化
- 日志限制在200条以内
- 计算属性使用缓存
- scroll-view 使用虚拟列表(长列表)
- 抽屉关闭时销毁内容
文档版本: v1.0 创建日期: 2026-01-21 基于设计文档: v2.0