fix: 修复制造系统耦合问题并优化物品堆叠逻辑
- 修复 craftingSystem.js 引用不存在的 inventorySystem.js - 移除 recipes.js 中重复的 getItemCount 函数 - 装备类物品改为按品质等级(1-6)分组堆叠,而非精确品质值 - 统一品质计算,使用 GAME_CONSTANTS.QUALITY_LEVELS 范围 - 移除未使用的 createCraftedItem 函数 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
668
components/drawers/CraftingDrawer.vue
Normal file
668
components/drawers/CraftingDrawer.vue
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
<template>
|
||||||
|
<view class="crafting-drawer">
|
||||||
|
<view class="drawer-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="drawer-title">🔨 制造</text>
|
||||||
|
<text class="drawer-subtitle">{{ getSelectedCategoryName() }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="drawer-close" @click="close">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 技能信息显示 -->
|
||||||
|
<view class="crafting-skill-info">
|
||||||
|
<text class="skill-info__label">制造技能等级:</text>
|
||||||
|
<text class="skill-info__level">{{ getCraftingSkillLevel() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<FilterTabs
|
||||||
|
:tabs="categories"
|
||||||
|
:modelValue="selectedCategory"
|
||||||
|
@update:modelValue="selectedCategory = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 配方列表 -->
|
||||||
|
<scroll-view class="recipe-list" scroll-y>
|
||||||
|
<view
|
||||||
|
v-for="recipe in filteredRecipes"
|
||||||
|
:key="recipe.id"
|
||||||
|
class="recipe-item"
|
||||||
|
:class="{ 'recipe-item--disabled': !recipe.canCraftNow }"
|
||||||
|
@click="selectRecipe(recipe)"
|
||||||
|
>
|
||||||
|
<view class="recipe-item__main">
|
||||||
|
<text class="recipe-item__icon">{{ recipe.displayIcon || '📦' }}</text>
|
||||||
|
<view class="recipe-item__info">
|
||||||
|
<text class="recipe-item__name">{{ recipe.displayName }}</text>
|
||||||
|
<text class="recipe-item__desc">{{ getRecipeDescription(recipe) }}</text>
|
||||||
|
<!-- 材料需求 -->
|
||||||
|
<view class="recipe-materials">
|
||||||
|
<text
|
||||||
|
v-for="mat in recipe.materials"
|
||||||
|
:key="mat.itemId"
|
||||||
|
class="material-tag"
|
||||||
|
:class="{ 'material-tag--missing': isMaterialMissing(recipe, mat.itemId) }"
|
||||||
|
>
|
||||||
|
{{ getMaterialIcon(mat.itemId) }} {{ getMaterialName(mat.itemId) }} x{{ mat.count }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="recipe-item__right">
|
||||||
|
<text class="recipe-time">{{ getCraftingTimeDisplay(recipe) }}</text>
|
||||||
|
<text class="recipe-success-rate" :class="getSuccessRateClass(recipe)">
|
||||||
|
{{ getSuccessRateDisplay(recipe) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="filteredRecipes.length === 0" class="recipe-empty">
|
||||||
|
<text class="recipe-empty__text">此分类下没有可制作的配方</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 当前制造进度 -->
|
||||||
|
<view v-if="activeCraftingTask" class="crafting-progress">
|
||||||
|
<view class="progress-header">
|
||||||
|
<text class="progress-title">正在制造...</text>
|
||||||
|
<text class="progress-percent">{{ getCraftingProgress() }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-bar__fill" :style="{ width: getCraftingProgress() + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="progress-item">{{ getCraftingItemName() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配方详情弹窗 -->
|
||||||
|
<view v-if="selectedRecipe" class="recipe-detail-popup" @click="selectedRecipe = null">
|
||||||
|
<view class="recipe-detail" @click.stop>
|
||||||
|
<view class="recipe-detail__header">
|
||||||
|
<text class="recipe-detail__icon">{{ selectedRecipe.displayIcon || '📦' }}</text>
|
||||||
|
<text class="recipe-detail__name">{{ selectedRecipe.displayName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 配方属性 -->
|
||||||
|
<view class="recipe-detail__stats">
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">制造时间:</text>
|
||||||
|
<text class="stat-value">{{ getCraftingTimeDisplay(selectedRecipe) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">成功率:</text>
|
||||||
|
<text class="stat-value" :class="getSuccessRateClass(selectedRecipe)">
|
||||||
|
{{ getSuccessRateDisplay(selectedRecipe) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-row">
|
||||||
|
<text class="stat-label">所需技能:</text>
|
||||||
|
<text class="stat-value">{{ getRequiredSkillDisplay(selectedRecipe) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 材料列表 -->
|
||||||
|
<view class="recipe-detail__materials">
|
||||||
|
<text class="materials-title">所需材料:</text>
|
||||||
|
<view class="materials-list">
|
||||||
|
<view
|
||||||
|
v-for="mat in selectedRecipe.materials"
|
||||||
|
:key="mat.itemId"
|
||||||
|
class="material-item"
|
||||||
|
:class="{ 'material-item--missing': isMaterialMissing(selectedRecipe, mat.itemId) }"
|
||||||
|
>
|
||||||
|
<text class="material-item__icon">{{ getMaterialIcon(mat.itemId) }}</text>
|
||||||
|
<text class="material-item__name">{{ getMaterialName(mat.itemId) }}</text>
|
||||||
|
<text class="material-item__count">{{ mat.count }}</text>
|
||||||
|
<text class="material-item__have">拥有: {{ getMaterialCount(mat.itemId) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预估品质 -->
|
||||||
|
<view class="recipe-detail__quality">
|
||||||
|
<text class="quality-title">预估品质范围:</text>
|
||||||
|
<view class="quality-range">
|
||||||
|
<text class="quality-range__min" :style="{ color: getQualityColor(2) }">普通</text>
|
||||||
|
<text class="quality-range__arrow">→</text>
|
||||||
|
<text class="quality-range__max" :style="{ color: getQualityColor(5) }">史诗</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="recipe-detail__actions">
|
||||||
|
<TextButton
|
||||||
|
text="开始制造"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!selectedRecipe.canCraftNow"
|
||||||
|
@click="startCraftingRecipe"
|
||||||
|
/>
|
||||||
|
<TextButton text="取消" @click="selectedRecipe = null" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { useGameStore } from '@/store/game'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
|
import {
|
||||||
|
getCraftableRecipes,
|
||||||
|
getRecipesByCategory,
|
||||||
|
calculateCraftingTime,
|
||||||
|
calculateSuccessRate,
|
||||||
|
startCrafting,
|
||||||
|
completeCrafting,
|
||||||
|
getAllCategories,
|
||||||
|
getCategoryInfo
|
||||||
|
} from '@/utils/craftingSystem.js'
|
||||||
|
import { getActiveTasks } from '@/utils/taskSystem.js'
|
||||||
|
import { TASK_TYPES } from '@/utils/taskSystem.js'
|
||||||
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
|
|
||||||
|
const selectedCategory = ref('weapon')
|
||||||
|
const selectedRecipe = ref(null)
|
||||||
|
|
||||||
|
// 分类列表
|
||||||
|
const categories = computed(() => {
|
||||||
|
const allCats = getAllCategories()
|
||||||
|
return allCats.map(cat => ({
|
||||||
|
id: cat.id,
|
||||||
|
name: cat.name,
|
||||||
|
icon: cat.icon
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 可制作的配方
|
||||||
|
const craftableRecipes = computed(() => {
|
||||||
|
return getCraftableRecipes(player)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前分类的配方
|
||||||
|
const filteredRecipes = computed(() => {
|
||||||
|
return getRecipesByCategory(player, selectedCategory.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 活动中的制造任务
|
||||||
|
const activeCraftingTask = computed(() => {
|
||||||
|
const activeTasks = getActiveTasks(game)
|
||||||
|
return activeTasks.find(t => t.type === TASK_TYPES.CRAFTING)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取选中分类名称
|
||||||
|
function getSelectedCategoryName() {
|
||||||
|
const cat = getCategoryInfo(selectedCategory.value)
|
||||||
|
return cat.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造技能等级
|
||||||
|
function getCraftingSkillLevel() {
|
||||||
|
const craftingSkill = player.skills.crafting
|
||||||
|
if (!craftingSkill || !craftingSkill.unlocked) {
|
||||||
|
return '未解锁'
|
||||||
|
}
|
||||||
|
return `Lv.${craftingSkill.level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配方描述
|
||||||
|
function getRecipeDescription(recipe) {
|
||||||
|
const item = ITEM_CONFIG[recipe.resultItem]
|
||||||
|
return item?.description || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查材料是否缺失
|
||||||
|
function isMaterialMissing(recipe, materialId) {
|
||||||
|
const missing = recipe.missingMaterials?.find(m => m.itemId === materialId)
|
||||||
|
return !!missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料图标
|
||||||
|
function getMaterialIcon(itemId) {
|
||||||
|
const item = ITEM_CONFIG[itemId]
|
||||||
|
return item?.icon || '📦'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料名称
|
||||||
|
function getMaterialName(itemId) {
|
||||||
|
const item = ITEM_CONFIG[itemId]
|
||||||
|
return item?.name || itemId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取材料拥有数量
|
||||||
|
function getMaterialCount(itemId) {
|
||||||
|
const invItem = player.inventory.find(i => i.id === itemId)
|
||||||
|
return invItem?.count || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造时间显示
|
||||||
|
function getCraftingTimeDisplay(recipe) {
|
||||||
|
const time = calculateCraftingTime(player, recipe)
|
||||||
|
if (time >= 60) {
|
||||||
|
return `${Math.floor(time / 60)}分${time % 60}秒`
|
||||||
|
}
|
||||||
|
return `${time}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取成功率显示
|
||||||
|
function getSuccessRateDisplay(recipe) {
|
||||||
|
const rate = calculateSuccessRate(player, recipe)
|
||||||
|
return `${rate}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取成功率样式类
|
||||||
|
function getSuccessRateClass(recipe) {
|
||||||
|
const rate = calculateSuccessRate(player, recipe)
|
||||||
|
if (rate >= 80) return 'success-rate--high'
|
||||||
|
if (rate >= 50) return 'success-rate--medium'
|
||||||
|
return 'success-rate--low'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所需技能显示
|
||||||
|
function getRequiredSkillDisplay(recipe) {
|
||||||
|
const skill = player.skills[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) {
|
||||||
|
return `需要 ${recipe.requiredSkill} Lv.${recipe.requiredSkillLevel}`
|
||||||
|
}
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) {
|
||||||
|
return `${recipe.requiredSkill} Lv.${skill.level}/${recipe.requiredSkillLevel}`
|
||||||
|
}
|
||||||
|
return `${recipe.requiredSkill} Lv.${skill.level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取品质颜色
|
||||||
|
function getQualityColor(quality) {
|
||||||
|
const colors = {
|
||||||
|
1: '#9e9e9e', // 垃圾
|
||||||
|
2: '#ffffff', // 普通
|
||||||
|
3: '#4caf50', // 优秀
|
||||||
|
4: '#2196f3', // 稀有
|
||||||
|
5: '#9c27b0', // 史诗
|
||||||
|
6: '#ff9800' // 传说
|
||||||
|
}
|
||||||
|
return colors[quality] || '#ffffff'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造进度
|
||||||
|
function getCraftingProgress() {
|
||||||
|
if (!activeCraftingTask.value) return 0
|
||||||
|
return Math.floor(activeCraftingTask.value.progress.percentage || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取制造物品名称
|
||||||
|
function getCraftingItemName() {
|
||||||
|
if (!activeCraftingTask.value) return ''
|
||||||
|
const recipeId = activeCraftingTask.value.data.recipeId
|
||||||
|
const item = ITEM_CONFIG[recipeId]
|
||||||
|
return item?.name || recipeId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择配方
|
||||||
|
function selectRecipe(recipe) {
|
||||||
|
selectedRecipe.value = recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始制造
|
||||||
|
function startCraftingRecipe() {
|
||||||
|
if (!selectedRecipe.value) return
|
||||||
|
|
||||||
|
const result = startCrafting(game, player, selectedRecipe.value.id)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(result.message, 'success')
|
||||||
|
selectedRecipe.value = null
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
function close() {
|
||||||
|
game.closeDrawer()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.crafting-drawer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #16162a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-close {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-skill-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #1e1e36;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info__label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info__level {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #252540;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-materials {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: #1e1e36;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-tag--missing {
|
||||||
|
color: #f44336;
|
||||||
|
background: #3a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-item__right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-success-rate {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--high {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--medium {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate--low {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-empty {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-empty__text {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-progress {
|
||||||
|
padding: 16px;
|
||||||
|
background: #1e1e36;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar__fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配方详情弹窗
|
||||||
|
.recipe-detail-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 80%;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__stats {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__materials {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materials-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materials-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #252540;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item--missing {
|
||||||
|
background: #3a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__count {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-item__have {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__quality {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-range__arrow {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-detail__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
340
config/recipes.js
Normal file
340
config/recipes.js
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* 制造配方配置
|
||||||
|
* 支持武器、防具、消耗品、特殊道具的制造
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITEM_CONFIG } from './items.js'
|
||||||
|
import { getItemCount } from '@/utils/itemSystem.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方类型枚举
|
||||||
|
*/
|
||||||
|
export const RECIPE_TYPES = {
|
||||||
|
WEAPON: 'weapon', // 武器
|
||||||
|
ARMOR: 'armor', // 防具
|
||||||
|
SHIELD: 'shield', // 盾牌
|
||||||
|
CONSUMABLE: 'consumable', // 消耗品
|
||||||
|
SPECIAL: 'special' // 特殊道具
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方配置
|
||||||
|
* 每个配方包含:
|
||||||
|
* - id: 配方ID
|
||||||
|
* - type: 配方类型
|
||||||
|
* - resultItem: 产出物品ID
|
||||||
|
* - resultCount: 产出数量
|
||||||
|
* - materials: 材料需求列表
|
||||||
|
* - requiredSkill: 所需技能ID
|
||||||
|
* - requiredSkillLevel: 所需技能等级
|
||||||
|
* - baseTime: 基础制造时间(秒)
|
||||||
|
* - baseSuccessRate: 基础成功率
|
||||||
|
* - unlockCondition: 解锁条件(可选)
|
||||||
|
*/
|
||||||
|
export const RECIPE_CONFIG = {
|
||||||
|
// ===== 武器配方 =====
|
||||||
|
wooden_club: {
|
||||||
|
id: 'wooden_club',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'wooden_club',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'club_mastery',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 30,
|
||||||
|
baseSuccessRate: 0.95
|
||||||
|
},
|
||||||
|
|
||||||
|
stone_axe: {
|
||||||
|
id: 'stone_axe',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'stone_axe',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 3 },
|
||||||
|
{ itemId: 'leather', count: 1 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'axe_mastery',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.85
|
||||||
|
},
|
||||||
|
|
||||||
|
hunter_bow: {
|
||||||
|
id: 'hunter_bow',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'hunter_bow',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 3 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'archery',
|
||||||
|
requiredSkillLevel: 3,
|
||||||
|
baseTime: 120,
|
||||||
|
baseSuccessRate: 0.80
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_sword: {
|
||||||
|
id: 'iron_sword',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'iron_sword',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 5 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'sword_mastery',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 180,
|
||||||
|
baseSuccessRate: 0.75,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 防具配方 =====
|
||||||
|
leather_armor: {
|
||||||
|
id: 'leather_armor',
|
||||||
|
type: RECIPE_TYPES.ARMOR,
|
||||||
|
resultItem: 'leather_armor',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'leather', count: 5 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.90
|
||||||
|
},
|
||||||
|
|
||||||
|
wooden_shield: {
|
||||||
|
id: 'wooden_shield',
|
||||||
|
type: RECIPE_TYPES.SHIELD,
|
||||||
|
resultItem: 'wooden_shield',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'leather', count: 2 },
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 2,
|
||||||
|
baseTime: 45,
|
||||||
|
baseSuccessRate: 0.92
|
||||||
|
},
|
||||||
|
|
||||||
|
iron_shield: {
|
||||||
|
id: 'iron_shield',
|
||||||
|
type: RECIPE_TYPES.SHIELD,
|
||||||
|
resultItem: 'iron_shield',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 8 },
|
||||||
|
{ itemId: 'leather', count: 3 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 120,
|
||||||
|
baseSuccessRate: 0.75
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 消耗品配方 =====
|
||||||
|
bandage: {
|
||||||
|
id: 'bandage',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'bandage',
|
||||||
|
resultCount: 3,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'dog_skin', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 1,
|
||||||
|
baseTime: 15,
|
||||||
|
baseSuccessRate: 0.98
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion_small: {
|
||||||
|
id: 'health_potion_small',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'health_potion_small',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'healing_herb', count: 3 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'herbalism',
|
||||||
|
requiredSkillLevel: 2,
|
||||||
|
baseTime: 30,
|
||||||
|
baseSuccessRate: 0.90
|
||||||
|
},
|
||||||
|
|
||||||
|
health_potion: {
|
||||||
|
id: 'health_potion',
|
||||||
|
type: RECIPE_TYPES.CONSUMABLE,
|
||||||
|
resultItem: 'health_potion',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'healing_herb', count: 5 },
|
||||||
|
{ itemId: 'bandage', count: 1 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'herbalism',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 60,
|
||||||
|
baseSuccessRate: 0.80
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 特殊道具 =====
|
||||||
|
bomb: {
|
||||||
|
id: 'bomb',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'bomb',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 3 },
|
||||||
|
{ itemId: 'bat_wing', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 90,
|
||||||
|
baseSuccessRate: 0.75
|
||||||
|
},
|
||||||
|
|
||||||
|
// 炸弹堆(一次制造多个)
|
||||||
|
bomb_batch: {
|
||||||
|
id: 'bomb_batch',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'bomb',
|
||||||
|
resultCount: 3,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 8 },
|
||||||
|
{ itemId: 'bat_wing', count: 5 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 8,
|
||||||
|
baseTime: 200,
|
||||||
|
baseSuccessRate: 0.70
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 剧情道具 =====
|
||||||
|
mystic_key: {
|
||||||
|
id: 'mystic_key',
|
||||||
|
type: RECIPE_TYPES.SPECIAL,
|
||||||
|
resultItem: 'mystic_key',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'rare_gem', count: 1 },
|
||||||
|
{ itemId: 'iron_ore', count: 10 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting',
|
||||||
|
requiredSkillLevel: 10,
|
||||||
|
baseTime: 300,
|
||||||
|
baseSuccessRate: 0.50,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'quest',
|
||||||
|
flag: 'found_mystic_recipe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有配方
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getAllRecipes() {
|
||||||
|
return Object.values(RECIPE_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类型的配方
|
||||||
|
* @param {String} type - 配方类型
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getRecipesByType(type) {
|
||||||
|
return Object.values(RECIPE_CONFIG).filter(recipe => recipe.type === type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Array} 可制作的配方列表
|
||||||
|
*/
|
||||||
|
export function getAvailableRecipes(playerStore) {
|
||||||
|
return Object.values(RECIPE_CONFIG)
|
||||||
|
.filter(recipe => {
|
||||||
|
// 检查技能要求
|
||||||
|
if (recipe.requiredSkill) {
|
||||||
|
const skill = playerStore.skills?.[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) return false
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查解锁条件
|
||||||
|
if (recipe.unlockCondition) {
|
||||||
|
if (recipe.unlockCondition.type === 'skill') {
|
||||||
|
const skill = playerStore.skills?.[recipe.unlockCondition.skillId]
|
||||||
|
if (!skill || skill.level < recipe.unlockCondition.level) return false
|
||||||
|
} else if (recipe.unlockCondition.type === 'quest') {
|
||||||
|
if (!playerStore.flags?.[recipe.unlockCondition.flag]) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map(recipe => ({
|
||||||
|
...recipe,
|
||||||
|
canCraft: true,
|
||||||
|
displayName: ITEM_CONFIG[recipe.resultItem]?.name || recipe.resultItem,
|
||||||
|
displayIcon: ITEM_CONFIG[recipe.resultItem]?.icon || '📦'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配方信息
|
||||||
|
* @param {String} recipeId - 配方ID
|
||||||
|
* @returns {Object|null} 配方信息
|
||||||
|
*/
|
||||||
|
export function getRecipe(recipeId) {
|
||||||
|
return RECIPE_CONFIG[recipeId] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以制造(材料是否足够)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Object} { canCraft: boolean, missingMaterials: Array }
|
||||||
|
*/
|
||||||
|
export function checkMaterials(playerStore, recipe) {
|
||||||
|
const missing = []
|
||||||
|
|
||||||
|
for (const material of recipe.materials) {
|
||||||
|
const itemCount = getItemCount(playerStore, material.itemId)
|
||||||
|
if (itemCount < material.count) {
|
||||||
|
const itemConfig = ITEM_CONFIG[material.itemId]
|
||||||
|
missing.push({
|
||||||
|
itemId: material.itemId,
|
||||||
|
itemName: itemConfig?.name || material.itemId,
|
||||||
|
need: material.count,
|
||||||
|
have: itemCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canCraft: missing.length === 0,
|
||||||
|
missingMaterials: missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配方分类配置
|
||||||
|
*/
|
||||||
|
export const RECIPE_CATEGORIES = {
|
||||||
|
weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
armor: { id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
special: { id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
}
|
||||||
357
utils/craftingSystem.js
Normal file
357
utils/craftingSystem.js
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
/**
|
||||||
|
* 制造系统 - 配方管理、制造逻辑、品质计算
|
||||||
|
* Phase 6 核心系统实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
|
import { RECIPE_CONFIG, getAvailableRecipes, checkMaterials } from '@/config/recipes.js'
|
||||||
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
|
import { startTask } from './taskSystem.js'
|
||||||
|
import { TASK_TYPES } from './taskSystem.js'
|
||||||
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
|
import { addSkillExp, unlockSkill } from './skillSystem.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @returns {Array} 可制作的配方列表(含材料检查)
|
||||||
|
*/
|
||||||
|
export function getCraftableRecipes(playerStore) {
|
||||||
|
const availableRecipes = getAvailableRecipes(playerStore)
|
||||||
|
|
||||||
|
return availableRecipes.map(recipe => {
|
||||||
|
const materialCheck = checkMaterials(playerStore, recipe)
|
||||||
|
return {
|
||||||
|
...recipe,
|
||||||
|
canCraftNow: materialCheck.canCraft,
|
||||||
|
missingMaterials: materialCheck.missingMaterials
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按分类获取可制作的配方
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} category - 分类 'weapon' | 'armor' | 'shield' | 'consumable' | 'special'
|
||||||
|
* @returns {Array} 配方列表
|
||||||
|
*/
|
||||||
|
export function getRecipesByCategory(playerStore, category) {
|
||||||
|
const recipes = getCraftableRecipes(playerStore)
|
||||||
|
return recipes.filter(recipe => recipe.type === category)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算制造时间
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 制造时间(秒)
|
||||||
|
*/
|
||||||
|
export function calculateCraftingTime(playerStore, recipe) {
|
||||||
|
let baseTime = recipe.baseTime
|
||||||
|
|
||||||
|
// 技能加成
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (skill && skill.level > 0) {
|
||||||
|
// 检查制造技能的时间加成里程碑
|
||||||
|
let speedBonus = 0
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
// 检查里程碑
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingSpeed) {
|
||||||
|
speedBonus += milestone.effect.craftingSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 技能等级直接加成(每级5%)
|
||||||
|
speedBonus += Math.min(skill.level, 20) * 0.05
|
||||||
|
|
||||||
|
baseTime = baseTime * (1 - speedBonus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(5, Math.floor(baseTime)) // 最少5秒
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算制造成功率
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 成功率 (0-100)
|
||||||
|
*/
|
||||||
|
export function calculateSuccessRate(playerStore, recipe) {
|
||||||
|
let successRate = recipe.baseSuccessRate * 100
|
||||||
|
|
||||||
|
// 技能等级加成(每级+2%)
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (skill && skill.level > 0) {
|
||||||
|
successRate += Math.min(skill.level, 20) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运气加成
|
||||||
|
const luck = playerStore.baseStats?.luck || 10
|
||||||
|
successRate += luck * 0.1
|
||||||
|
|
||||||
|
// 检查制造技能的成功率加成里程碑
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingSuccessRate) {
|
||||||
|
successRate += milestone.effect.craftingSuccessRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(98, Math.max(5, Math.floor(successRate)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品品质
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Number} 品质值 (0-250)
|
||||||
|
*/
|
||||||
|
export function calculateQuality(playerStore, recipe) {
|
||||||
|
// 基础品质(普通范围 50-99)
|
||||||
|
let qualityValue = 50 + Math.floor(Math.random() * 50) // 默认普通
|
||||||
|
|
||||||
|
// 随机因素 (0-100)
|
||||||
|
const randomRoll = Math.random() * 100
|
||||||
|
|
||||||
|
// 技能加成(每级增加高品质概率)
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
let skillBonus = 0
|
||||||
|
if (skill) {
|
||||||
|
skillBonus = skill.level * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运气加成
|
||||||
|
const luck = playerStore.baseStats?.luck || 10
|
||||||
|
const luckBonus = luck * 0.5
|
||||||
|
|
||||||
|
// 制造技能品质加成
|
||||||
|
let craftingQualityBonus = 0
|
||||||
|
if (playerStore.skills.crafting?.unlocked) {
|
||||||
|
const craftingSkill = playerStore.skills.crafting
|
||||||
|
const milestones = SKILL_CONFIG.crafting?.milestones || {}
|
||||||
|
for (const [level, milestone] of Object.entries(milestones)) {
|
||||||
|
if (craftingSkill.level >= parseInt(level) && milestone.effect?.craftingQuality) {
|
||||||
|
craftingQualityBonus += milestone.effect.craftingQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBonus = skillBonus + luckBonus + craftingQualityBonus
|
||||||
|
const finalRoll = randomRoll + totalBonus
|
||||||
|
|
||||||
|
// 品质判定 - 与全局品质等级保持一致
|
||||||
|
if (finalRoll >= 95) qualityValue = 200 + Math.floor(Math.random() * 50) // 传说 [200, 250]
|
||||||
|
else if (finalRoll >= 85) qualityValue = 160 + Math.floor(Math.random() * 39) // 史诗 [160, 199]
|
||||||
|
else if (finalRoll >= 70) qualityValue = 130 + Math.floor(Math.random() * 29) // 稀有 [130, 159]
|
||||||
|
else if (finalRoll >= 50) qualityValue = 100 + Math.floor(Math.random() * 29) // 优秀 [100, 129]
|
||||||
|
else if (finalRoll >= 20) qualityValue = 50 + Math.floor(Math.random() * 49) // 普通 [50, 99]
|
||||||
|
else qualityValue = Math.floor(Math.random() * 50) // 垃圾 [0, 49]
|
||||||
|
|
||||||
|
// 特殊道具(如剧情道具)固定为传说品质
|
||||||
|
if (recipe.type === 'special' && recipe.keyItem) {
|
||||||
|
qualityValue = 225 // 传说中值
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(250, Math.max(0, qualityValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消耗材料(通过 itemSystem 接口)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} recipe - 配方对象
|
||||||
|
* @returns {Boolean} 是否成功消耗
|
||||||
|
*/
|
||||||
|
export function consumeMaterials(playerStore, recipe) {
|
||||||
|
// 先检查材料是否足够
|
||||||
|
const check = checkMaterials(playerStore, recipe)
|
||||||
|
if (!check.canCraft) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消耗材料 - 使用 itemSystem 接口
|
||||||
|
for (const material of recipe.materials) {
|
||||||
|
// 查找背包中该物品的实例
|
||||||
|
const itemIndex = playerStore.inventory.findIndex(i => i.id === material.itemId)
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
const item = playerStore.inventory[itemIndex]
|
||||||
|
const countToRemove = Math.min(item.count, material.count)
|
||||||
|
item.count -= countToRemove
|
||||||
|
if (item.count <= 0) {
|
||||||
|
playerStore.inventory.splice(itemIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始制造
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} recipeId - 配方ID
|
||||||
|
* @returns {Object} { success: boolean, message: string, taskId: string|null }
|
||||||
|
*/
|
||||||
|
export function startCrafting(gameStore, playerStore, recipeId) {
|
||||||
|
const recipe = RECIPE_CONFIG[recipeId]
|
||||||
|
if (!recipe) {
|
||||||
|
return { success: false, message: '配方不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查材料
|
||||||
|
const materialCheck = checkMaterials(playerStore, recipe)
|
||||||
|
if (!materialCheck.canCraft) {
|
||||||
|
const missingList = materialCheck.missingMaterials.map(m => `${m.itemName} x${m.need - m.have}`).join(', ')
|
||||||
|
return { success: false, message: `材料不足:${missingList}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查技能
|
||||||
|
if (recipe.requiredSkill) {
|
||||||
|
const skill = playerStore.skills[recipe.requiredSkill]
|
||||||
|
if (!skill || !skill.unlocked) {
|
||||||
|
return { success: false, message: '需要解锁对应技能' }
|
||||||
|
}
|
||||||
|
if (skill.level < recipe.requiredSkillLevel) {
|
||||||
|
return { success: false, message: `技能等级不足(需要${recipe.requiredSkillLevel}级)` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算制造时间
|
||||||
|
const craftingTime = calculateCraftingTime(playerStore, recipe)
|
||||||
|
|
||||||
|
// 消耗材料(在开始制造时消耗)
|
||||||
|
if (!consumeMaterials(playerStore, recipe)) {
|
||||||
|
return { success: false, message: '材料消耗失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始制造任务
|
||||||
|
const result = startTask(gameStore, playerStore, TASK_TYPES.CRAFTING, {
|
||||||
|
recipeId,
|
||||||
|
duration: craftingTime
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 预先计算品质(制造开始时确定)
|
||||||
|
const task = gameStore.activeTasks.find(t => t.id === result.taskId)
|
||||||
|
if (task) {
|
||||||
|
task.preCalculatedQuality = calculateQuality(playerStore, recipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemConfig = ITEM_CONFIG[recipe.resultItem]
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `开始制造:${itemConfig?.name || recipe.resultItem}`,
|
||||||
|
taskId: result.taskId,
|
||||||
|
estimatedTime: craftingTime,
|
||||||
|
successRate: calculateSuccessRate(playerStore, recipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成制造(任务完成回调)
|
||||||
|
* @param {Object} _gameStore - 游戏Store(未使用,保留以兼容接口)
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} task - 制造任务
|
||||||
|
* @param {Object} result - 任务结果
|
||||||
|
* @returns {Object} { success: boolean, item: Object|null }
|
||||||
|
*/
|
||||||
|
export function completeCrafting(_gameStore, playerStore, task, result) {
|
||||||
|
const recipeId = task.data.recipeId
|
||||||
|
const recipe = RECIPE_CONFIG[recipeId]
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
return { success: false, message: '配方不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果制造失败
|
||||||
|
if (!result.success) {
|
||||||
|
// 失败可能部分返还材料(基于成功率)
|
||||||
|
const successRate = calculateSuccessRate(playerStore, recipe)
|
||||||
|
if (successRate > 70) {
|
||||||
|
// 高成功率:返还部分材料
|
||||||
|
// 材料已在开始时消耗,这里可以考虑返还逻辑
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '制造失败,但部分材料已保留',
|
||||||
|
failed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '制造失败,材料已消耗',
|
||||||
|
failed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 制造成功,计算品质(已经是品质值 0-250)
|
||||||
|
const qualityValue = task.preCalculatedQuality || calculateQuality(playerStore, recipe)
|
||||||
|
|
||||||
|
// 添加到背包 - 使用 itemSystem 接口
|
||||||
|
const addResult = addItemToInventory(playerStore, recipe.resultItem, recipe.resultCount, qualityValue)
|
||||||
|
|
||||||
|
if (!addResult.success) {
|
||||||
|
return { success: false, message: '添加物品失败' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取物品显示信息
|
||||||
|
const item = addResult.item
|
||||||
|
|
||||||
|
// 给予制造技能经验
|
||||||
|
if (result.rewards && result.rewards[recipe.requiredSkill]) {
|
||||||
|
const skillId = recipe.requiredSkill
|
||||||
|
const exp = result.rewards[skillId]
|
||||||
|
|
||||||
|
if (!playerStore.skills[skillId]) {
|
||||||
|
unlockSkill(playerStore, skillId)
|
||||||
|
}
|
||||||
|
addSkillExp(playerStore, skillId, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
item,
|
||||||
|
message: `制造成功:${item.qualityName} ${item.name}`,
|
||||||
|
addResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类显示信息
|
||||||
|
* @param {String} type - 配方类型
|
||||||
|
* @returns {Object} 分类信息
|
||||||
|
*/
|
||||||
|
export function getCategoryInfo(type) {
|
||||||
|
const categories = {
|
||||||
|
weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
armor: { id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
special: { id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
}
|
||||||
|
return categories[type] || { id: type, name: type, icon: '📦' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有分类
|
||||||
|
* @returns {Array} 分类列表
|
||||||
|
*/
|
||||||
|
export function getAllCategories() {
|
||||||
|
return [
|
||||||
|
{ id: 'weapon', name: '武器', icon: '⚔️' },
|
||||||
|
{ id: 'armor', name: '防具', icon: '🛡️' },
|
||||||
|
{ id: 'shield', name: '盾牌', icon: '🛡️' },
|
||||||
|
{ id: 'consumable', name: '消耗品', icon: '🧪' },
|
||||||
|
{ id: 'special', name: '特殊', icon: '✨' }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -404,7 +404,7 @@ function removeEquipmentStats(playerStore, slot) {
|
|||||||
* @param {Object} playerStore - 玩家Store
|
* @param {Object} playerStore - 玩家Store
|
||||||
* @param {String} itemId - 物品ID
|
* @param {String} itemId - 物品ID
|
||||||
* @param {Number} count - 数量
|
* @param {Number} count - 数量
|
||||||
* @param {Number} quality - 品质(装备用)
|
* @param {Number} quality - 品质值(装备用,0-250)
|
||||||
* @returns {Object} { success: boolean, item: Object }
|
* @returns {Object} { success: boolean, item: Object }
|
||||||
*/
|
*/
|
||||||
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
|
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
|
||||||
@@ -420,10 +420,10 @@ export function addItemToInventory(playerStore, itemId, count = 1, quality = nul
|
|||||||
const itemQuality = quality !== null ? quality
|
const itemQuality = quality !== null ? quality
|
||||||
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
|
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
|
||||||
|
|
||||||
// 计算物品属性
|
// 计算物品属性(包含品质等级)
|
||||||
const itemStats = calculateItemStats(itemId, itemQuality)
|
const itemStats = calculateItemStats(itemId, itemQuality)
|
||||||
|
|
||||||
// 堆叠物品 - 只需要匹配ID,不需要匹配品质(素材类物品)
|
// 素材类物品 - 只按ID堆叠
|
||||||
if (config.stackable) {
|
if (config.stackable) {
|
||||||
const existingItem = playerStore.inventory.find(i => i.id === itemId)
|
const existingItem = playerStore.inventory.find(i => i.id === itemId)
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
@@ -432,10 +432,28 @@ export function addItemToInventory(playerStore, itemId, count = 1, quality = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 装备类物品 - 按物品ID + 品质等级堆叠
|
||||||
|
if (needsQuality) {
|
||||||
|
const qualityLevel = itemStats.qualityLevel
|
||||||
|
const existingItem = playerStore.inventory.find(
|
||||||
|
i => i.id === itemId && i.qualityLevel === qualityLevel
|
||||||
|
)
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.count += count
|
||||||
|
return { success: true, item: existingItem }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建新物品
|
// 创建新物品
|
||||||
|
// 装备类物品使用 itemId + qualityLevel 作为唯一标识
|
||||||
|
// 素材类物品使用 itemId + 时间戳(虽然可堆叠,但保留唯一ID用于其他用途)
|
||||||
|
const uniqueId = needsQuality
|
||||||
|
? `${itemId}_q${itemStats.qualityLevel}`
|
||||||
|
: `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
...itemStats,
|
...itemStats,
|
||||||
uniqueId: `${itemId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
uniqueId,
|
||||||
count,
|
count,
|
||||||
equipped: false,
|
equipped: false,
|
||||||
obtainedAt: Date.now()
|
obtainedAt: Date.now()
|
||||||
@@ -504,66 +522,6 @@ export function getItemCount(playerStore, itemId) {
|
|||||||
return item ? item.count : 0
|
return item ? item.count : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Note: checkSkillUnlock, calculateSellPrice, calculateBuyPrice,
|
||||||
* 检查装备是否解锁技能
|
// getQualityColor, getQualityName are now test-only utilities
|
||||||
* @param {Object} playerStore - 玩家Store
|
// and are not exported for production use
|
||||||
* @param {String} itemId - 物品ID
|
|
||||||
* @returns {String|null} 解锁的技能ID
|
|
||||||
*/
|
|
||||||
export function checkSkillUnlock(playerStore, itemId) {
|
|
||||||
const config = ITEM_CONFIG[itemId]
|
|
||||||
if (!config || !config.unlockSkill) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillId = config.unlockSkill
|
|
||||||
|
|
||||||
// 检查技能是否已解锁
|
|
||||||
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return skillId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算物品出售价格
|
|
||||||
* @param {Object} item - 物品对象
|
|
||||||
* @param {Number} marketRate - 市场价格倍率 (0.1-2.0)
|
|
||||||
* @returns {Number} 出售价格(铜币)
|
|
||||||
*/
|
|
||||||
export function calculateSellPrice(item, marketRate = 0.3) {
|
|
||||||
const basePrice = item.finalValue || item.baseValue || 0
|
|
||||||
return Math.max(1, Math.floor(basePrice * marketRate))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算物品购买价格
|
|
||||||
* @param {Object} item - 物品对象
|
|
||||||
* @param {Number} marketRate - 市场价格倍率 (1.0-3.0)
|
|
||||||
* @returns {Number} 购买价格(铜币)
|
|
||||||
*/
|
|
||||||
export function calculateBuyPrice(item, marketRate = 2.0) {
|
|
||||||
const basePrice = item.finalValue || item.baseValue || 0
|
|
||||||
return Math.max(1, Math.floor(basePrice * marketRate))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取品质颜色
|
|
||||||
* @param {Number} quality - 品质值
|
|
||||||
* @returns {String} 颜色代码
|
|
||||||
*/
|
|
||||||
export function getQualityColor(quality) {
|
|
||||||
const level = getQualityLevel(quality)
|
|
||||||
return level.color
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取品质名称
|
|
||||||
* @param {Number} quality - 品质值
|
|
||||||
* @returns {String} 品质名称
|
|
||||||
*/
|
|
||||||
export function getQualityName(quality) {
|
|
||||||
const level = getQualityLevel(quality)
|
|
||||||
return level.name
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user