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

72 KiB
Raw Permalink Blame History

开发步骤文档

基于设计文档 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. 在项目根目录执行:
    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 新游戏流程测试

测试步骤

  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