feat: 完善数学模型和品质系统

- 修复品质等级范围重叠问题(优秀/稀有无重叠)
- 统一 formulas.js 与 constants.js 的品质判定
- 修复经验公式与游戏实际逻辑不一致
- 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10%
- 添加数学模型文档和README
- 添加数值验证脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-25 14:54:20 +08:00
parent cef974d94f
commit 5d4371ba1f
9 changed files with 2459 additions and 82 deletions

View File

@@ -73,30 +73,45 @@
<view class="equipment-slots">
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
<text class="equipment-slot__label">武器</text>
<text v-if="player.equipment.weapon" class="equipment-slot__item">
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
</text>
<view v-if="player.equipment.weapon" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.weapon.qualityColor }">
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
</text>
<text class="equipment-slot__stats">{{ player.equipment.weapon.finalDamage }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.weapon.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
<text class="equipment-slot__label">防具</text>
<text v-if="player.equipment.armor" class="equipment-slot__item">
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
</text>
<view v-if="player.equipment.armor" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.armor.qualityColor }">
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
</text>
<text class="equipment-slot__stats">{{ player.equipment.armor.finalDefense }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.armor.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
<text class="equipment-slot__label">盾牌</text>
<text v-if="player.equipment.shield" class="equipment-slot__item">
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
</text>
<view v-if="player.equipment.shield" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.shield.qualityColor }">
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
</text>
<text class="equipment-slot__stats">格挡{{ player.equipment.shield.finalShield }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.shield.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
<text class="equipment-slot__label">饰品</text>
<text v-if="player.equipment.accessory" class="equipment-slot__item">
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
</text>
<view v-if="player.equipment.accessory" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.accessory.qualityColor }">
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
</text>
<text class="equipment-slot__quality">[{{ player.equipment.accessory.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
</view>
@@ -127,19 +142,47 @@
<!-- 技能列表 -->
<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>
<view class="skill-item__info">
<text class="skill-item__name">{{ skill.name }}</text>
<text class="skill-item__level">Lv.{{ skill.level }}</text>
<view class="skill-item__main" @click="toggleSkillDetail(skill.id)">
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
<view class="skill-item__info">
<text class="skill-item__name">{{ skill.name }}</text>
<text class="skill-item__level">Lv.{{ skill.level }}/{{ skill.maxLevel }}</text>
</view>
<view class="skill-item__bar">
<ProgressBar
:value="skill.exp"
:max="skill.maxExp"
height="8rpx"
:showText="false"
/>
<text class="skill-item__progress">{{ Math.floor(skill.exp) }}/{{ Math.floor(skill.maxExp) }}</text>
</view>
<text class="skill-item__expand">{{ expandedSkills.includes(skill.id) ? '▼' : '▶' }}</text>
</view>
<view class="skill-item__bar">
<ProgressBar
:value="skill.exp"
:max="skill.maxExp"
height="8rpx"
:showText="false"
/>
<text class="skill-item__progress">{{ skill.exp }}/{{ skill.maxExp }}</text>
<!-- 技能详情展开后显示 -->
<view v-if="expandedSkills.includes(skill.id)" class="skill-item__detail">
<!-- 已获得的增幅 -->
<view v-if="getActiveMilestones(skill).length > 0" class="skill-detail__section">
<text class="skill-detail__title"> 已获得增幅</text>
<view v-for="m in getActiveMilestones(skill)" :key="m.level" class="skill-detail__bonus">
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
</view>
</view>
<!-- 即将获得的增幅 -->
<view v-if="getUpcomingMilestones(skill).length > 0" class="skill-detail__section">
<text class="skill-detail__title">即将解锁</text>
<view v-for="m in getUpcomingMilestones(skill)" :key="m.level" class="skill-detail__bonus upcoming">
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
</view>
</view>
<view v-if="getActiveMilestones(skill).length === 0 && getUpcomingMilestones(skill).length === 0" class="skill-detail__empty">
<text class="skill-detail__empty-text">暂无增幅效果</text>
</view>
</view>
</view>
<view v-if="filteredSkills.length === 0" class="skill-empty">
@@ -277,6 +320,7 @@ const currentSkillFilter = ref('all')
const showBooks = ref(false)
const showTraining = ref(false)
const showTasks = ref(true)
const expandedSkills = ref([]) // 展开的技能ID列表
const skillFilterTabs = [
{ id: 'all', label: '全部' },
@@ -382,6 +426,42 @@ const readingTimeText = computed(() => {
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
})
// 技能详情相关函数
function toggleSkillDetail(skillId) {
const index = expandedSkills.value.indexOf(skillId)
if (index > -1) {
expandedSkills.value.splice(index, 1)
} else {
expandedSkills.value.push(skillId)
}
}
function getActiveMilestones(skill) {
const config = SKILL_CONFIG[skill.id]
if (!config || !config.milestones) return []
return Object.entries(config.milestones)
.filter(([level]) => parseInt(level) <= skill.level)
.map(([level, milestone]) => ({
level: parseInt(level),
desc: milestone.desc
}))
.sort((a, b) => a.level - b.level)
}
function getUpcomingMilestones(skill) {
const config = SKILL_CONFIG[skill.id]
if (!config || !config.milestones) return []
return Object.entries(config.milestones)
.filter(([level]) => parseInt(level) > skill.level)
.map(([level, milestone]) => ({
level: parseInt(level),
desc: milestone.desc
}))
.sort((a, b) => a.level - b.level)
}
function startReading(book) {
const result = startTask(game, player, 'reading', {
itemId: book.id,
@@ -598,12 +678,19 @@ function cancelActiveTask(task) {
.skill-item {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
flex-direction: column;
background-color: $bg-secondary;
border-radius: 8rpx;
margin-bottom: 8rpx;
overflow: hidden;
&__main {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
cursor: pointer;
}
&__icon {
font-size: 32rpx;
@@ -641,6 +728,72 @@ function cancelActiveTask(task) {
font-size: 20rpx;
text-align: right;
}
&__expand {
color: $text-muted;
font-size: 20rpx;
padding-left: 8rpx;
}
&__detail {
padding: 12rpx;
padding-top: 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
}
.skill-detail {
&__section {
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
}
&__title {
color: $text-primary;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 8rpx;
display: block;
}
&__bonus {
display: flex;
align-items: center;
gap: 12rpx;
padding: 6rpx 12rpx;
background-color: rgba(78, 205, 196, 0.1);
border-radius: 4rpx;
margin-bottom: 6rpx;
&.upcoming {
background-color: rgba(255, 107, 107, 0.1);
}
}
&__bonus-level {
color: $accent;
font-size: 22rpx;
font-weight: bold;
min-width: 60rpx;
}
&__bonus-desc {
color: $text-secondary;
font-size: 22rpx;
}
&__empty {
padding: 16rpx;
text-align: center;
}
&__empty-text {
color: $text-muted;
font-size: 24rpx;
}
}
.skill-empty {
@@ -687,8 +840,26 @@ function cancelActiveTask(task) {
}
&__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rpx;
}
&__name {
color: $text-primary;
font-size: 22rpx;
text-align: center;
}
&__stats {
color: $accent;
font-size: 20rpx;
}
&__quality {
color: $text-muted;
font-size: 18rpx;
}
&__empty {