Browse Source

增加一个角色信息的详细显示页面

master
秦汉 1 month ago
parent
commit
03826b4956
  1. 10
      Build_God_Api/Build_God_Api/Dto/CharacterDto.cs
  2. 1
      Build_God_Api/Build_God_Api/Program.cs
  3. 30
      Build_God_Api/Build_God_Api/Services/CharacterService.cs
  4. 139
      Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs
  5. 18
      Build_God_Api/Build_God_Api/Services/ScrapService.cs
  6. 8
      Build_God_Game/src/api/character.ts
  7. 260
      Build_God_Game/src/views/GameView.vue

10
Build_God_Api/Build_God_Api/Dto/CharacterDto.cs

@ -7,10 +7,18 @@
public string LevelName { get; set; }
public int LevelId { get; set; }
public decimal MaxHP { get; set; }
public decimal BaseMaxHP { get; set; }
public decimal BonusMaxHP { get; set; }
public decimal CurrentHP { get; set; }
public decimal Attack { get; set; }
public decimal BaseAttack { get; set; }
public decimal BonusAttack { get; set; }
public decimal Defend { get; set; }
public decimal BaseDefend { get; set; }
public decimal BonusDefend { get; set; }
public decimal CriticalRate { get; set; }
public decimal BaseCriticalRate { get; set; }
public decimal BonusCriticalRate { get; set; }
public string ProfessionName { get; set; }
public decimal Money { get; set; }
public decimal CurrentExp { get; set; }
@ -26,4 +34,4 @@
public DateTime LastLogin { get; set; }
public DateTime CreatedOn { get; set; }
}
}
}

1
Build_God_Api/Build_God_Api/Program.cs

@ -96,7 +96,6 @@ namespace Build_God_Api
sqlSugarClient.CodeFirst.InitTables(typeof(ChatMessage));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterDailyMission));
sqlSugarClient.CodeFirst.InitTables(typeof(Scrap));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterScrap));
sqlSugarClient.CodeFirst.InitTables(typeof(Monster));
sqlSugarClient.CodeFirst.InitTables(typeof(MonsterReward));

30
Build_God_Api/Build_God_Api/Services/CharacterService.cs

@ -276,10 +276,18 @@ namespace Build_God_Api.Services
LevelName = level?.Name ?? "未知",
LevelId = c.LevelId,
MaxHP = attrs.MaxHP,
BaseMaxHP = attrs.BaseMaxHP,
BonusMaxHP = attrs.BonusMaxHP,
CurrentHP = c.CurrentHP,
Attack = attrs.Attack,
BaseAttack = attrs.BaseAttack,
BonusAttack = attrs.BonusAttack,
Defend = attrs.Defend,
BaseDefend = attrs.BaseDefend,
BonusDefend = attrs.BonusDefend,
CriticalRate = attrs.CriticalRate,
BaseCriticalRate = attrs.BaseCriticalRate,
BonusCriticalRate = attrs.BonusCriticalRate,
ProfessionName = profession?.Name,
Money = c.Money,
CurrentExp = c.CurrentExp,
@ -340,7 +348,27 @@ namespace Build_God_Api.Services
LastLogin = DateTime.Now
};
await db.Insertable(newOne).ExecuteCommandAsync();
var characterId = await db.Insertable(newOne).ExecuteReturnIdentityAsync();
var bag = await db.Queryable<Bag>().FirstAsync(x => x.Rarity == BagRarity.Common);
if (bag == null)
{
bag = new Bag
{
Name = "新手背包",
Rarity = BagRarity.Common,
Capacity = 50,
Description = "新手的初始背包"
};
await db.Insertable(bag).ExecuteCommandAsync();
}
var characterBag = new CharacterBag
{
CharacterId = characterId,
BagId = bag.Id
};
await db.Insertable(characterBag).ExecuteCommandAsync();
return true;
}

139
Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs

@ -1,5 +1,6 @@
using Build_God_Api.DB;
using SqlSugar;
using System.Text.Json;
namespace Build_God_Api.Services.Game
{
@ -16,10 +17,18 @@ namespace Build_God_Api.Services.Game
public class CharacterAttributes
{
public decimal MaxHP { get; set; }
public decimal BaseMaxHP { get; set; }
public decimal BonusMaxHP { get; set; }
public decimal CurrentHP { get; set; }
public decimal Attack { get; set; }
public decimal BaseAttack { get; set; }
public decimal BonusAttack { get; set; }
public decimal Defend { get; set; }
public decimal BaseDefend { get; set; }
public decimal BonusDefend { get; set; }
public decimal CriticalRate { get; set; }
public decimal BaseCriticalRate { get; set; }
public decimal BonusCriticalRate { get; set; }
}
public class CharacterAttributeCalculateService(ISqlSugarClient context): ICharacterAttributeCalculateService
@ -44,6 +53,85 @@ namespace Build_God_Api.Services.Game
return 0.5m * character.LevelId * character.LevelId * (decimal)Math.Log((double)(character.CurrentExp + 1000)) * defendRate;
}
private async Task<(decimal AttackBonus, decimal DefendBonus, decimal HPBonus)> CalculateScrapBonusAsync(int characterBagId)
{
var bagItems = await _context.Queryable<BagItem>()
.Where(x => x.CharacterBagId == characterBagId && x.ItemType == BagItemType.Scrap)
.ToListAsync();
decimal attackBonus = 0;
decimal defendBonus = 0;
decimal hpBonus = 0;
foreach (var item in bagItems)
{
var scrap = await _context.Queryable<Scrap>().FirstAsync(x => x.Id == item.ItemId);
if (scrap != null)
{
attackBonus += scrap.AttackBonus * item.Quantity;
defendBonus += scrap.DefenseBonus * item.Quantity;
hpBonus += scrap.HpBonus * item.Quantity;
}
}
return (attackBonus, defendBonus, hpBonus);
}
private async Task<(decimal AttackBonus, decimal DefendBonus, decimal HPBonus, decimal CriticalBonus)> CalculateEquipmentBonusAsync(int characterBagId)
{
var equipmentInstances = await _context.Queryable<EquipmentInstance>()
.Where(x => x.CharacterBagId == characterBagId)
.ToListAsync();
decimal attackBonus = 0;
decimal defendBonus = 0;
decimal hpBonus = 0;
decimal criticalBonus = 0;
foreach (var instance in equipmentInstances)
{
var template = await _context.Queryable<EquipmentTemplate>().FirstAsync(x => x.Id == instance.EquipmentTemplateId);
if (template == null || string.IsNullOrEmpty(instance.Attributes)) continue;
try
{
var attributes = JsonSerializer.Deserialize<List<EquipmentAttributeDto>>(instance.Attributes);
if (attributes == null) continue;
foreach (var attr in attributes)
{
switch (attr.Type)
{
case EquipmentAttributeType.AttackFixed:
attackBonus += attr.Value;
break;
case EquipmentAttributeType.AttackPercent:
break;
case EquipmentAttributeType.DefendFixed:
defendBonus += attr.Value;
break;
case EquipmentAttributeType.DefendPercent:
break;
case EquipmentAttributeType.HealthBonusFixed:
hpBonus += attr.Value;
break;
case EquipmentAttributeType.HealthBonusPercent:
break;
case EquipmentAttributeType.CriticalPercent:
criticalBonus += attr.Value;
break;
}
}
}
catch
{
// ignore parse errors
}
}
return (attackBonus, defendBonus, hpBonus, criticalBonus);
}
public async Task<CharacterAttributes> CalculateAttributesAsync(Character character)
{
Profession? profession = null;
@ -52,22 +140,43 @@ namespace Build_God_Api.Services.Game
profession = await _context.Queryable<Profession>().FirstAsync(x => x.Id == character.ProfessionId);
}
decimal attackRate = profession?.AttackRate ?? 1m;
decimal defendRate = profession?.DefendRate ?? 1m;
decimal criticalRate = profession?.CriticalRate ?? 1m;
decimal baseMaxHP = CalculateMaxHP(character, profession);
decimal baseAttack = CalculateAttack(character, profession);
decimal baseDefend = CalculateDefend(character, profession);
decimal baseCritical = 0;
decimal maxHP = CalculateMaxHP(character, profession);
decimal attack = CalculateAttack(character, profession);
decimal defend = CalculateDefend(character, profession);
decimal critRate = 0;
decimal bonusAttack = 0;
decimal bonusDefend = 0;
decimal bonusHP = 0;
decimal bonusCritical = 0;
var characterBag = await _context.Queryable<CharacterBag>().FirstAsync(x => x.CharacterId == character.Id);
if (characterBag != null)
{
var (scrapAttack, scrapDefend, scrapHP) = await CalculateScrapBonusAsync(characterBag.Id);
var (equipAttack, equipDefend, equipHP, equipCritical) = await CalculateEquipmentBonusAsync(characterBag.Id);
bonusAttack = scrapAttack + equipAttack;
bonusDefend = scrapDefend + equipDefend;
bonusHP = scrapHP + equipHP;
bonusCritical = equipCritical;
}
return new CharacterAttributes
{
MaxHP = maxHP,
MaxHP = baseMaxHP + bonusHP,
BaseMaxHP = baseMaxHP,
BonusMaxHP = bonusHP,
CurrentHP = character.CurrentHP,
Attack = attack,
Defend = defend,
CriticalRate = critRate
Attack = baseAttack + bonusAttack,
BaseAttack = baseAttack,
BonusAttack = bonusAttack,
Defend = baseDefend + bonusDefend,
BaseDefend = baseDefend,
BonusDefend = bonusDefend,
CriticalRate = baseCritical + bonusCritical,
BaseCriticalRate = baseCritical,
BonusCriticalRate = bonusCritical
};
}
@ -85,4 +194,10 @@ namespace Build_God_Api.Services.Game
await _context.Updateable(character).ExecuteCommandAsync();
}
}
}
public class EquipmentAttributeDto
{
public EquipmentAttributeType Type { get; set; }
public decimal Value { get; set; }
}
}

18
Build_God_Api/Build_God_Api/Services/ScrapService.cs

@ -115,22 +115,30 @@ namespace Build_God_Api.Services
public async Task<List<ScrapHistoryDto>> GetCharacterScrapHistoryAsync(int characterId)
{
var history = await db.Queryable<CharacterScrap>()
.Where(x => x.CharacterId == characterId)
.OrderByDescending(x => x.ObtainedAt)
var characterBag = await db.Queryable<CharacterBag>()
.FirstAsync(x => x.CharacterId == characterId);
if (characterBag == null)
{
return new List<ScrapHistoryDto>();
}
var history = await db.Queryable<BagItem>()
.Where(x => x.CharacterBagId == characterBag.Id && x.ItemType == BagItemType.Scrap)
.OrderByDescending(x => x.CreatedOn)
.ToListAsync();
var result = new List<ScrapHistoryDto>();
foreach (var item in history)
{
var scrap = await db.Queryable<Scrap>().FirstAsync(x => x.Id == item.ScrapId);
var scrap = await db.Queryable<Scrap>().FirstAsync(x => x.Id == item.ItemId);
if (scrap != null)
{
result.Add(new ScrapHistoryDto
{
Id = item.Id,
Scrap = MapToDto(scrap),
ObtainedAt = item.ObtainedAt
ObtainedAt = item.CreatedOn
});
}
}

8
Build_God_Game/src/api/character.ts

@ -5,10 +5,18 @@ export interface CharacterDto {
levelName: string
levelId: number
maxHP: number
baseMaxHP: number
bonusMaxHP: number
currentHP: number
attack: number
baseAttack: number
bonusAttack: number
defend: number
baseDefend: number
bonusDefend: number
criticalRate: number
baseCriticalRate: number
bonusCriticalRate: number
professionName?: string
money: number
currentExp: number

260
Build_God_Game/src/views/GameView.vue

@ -74,6 +74,8 @@ const navigateTo = (item: { label: string }) => {
router.push('/bag')
} else if (item.label === '捡垃圾') {
router.push('/scrap')
} else if (item.label === '角色') {
showCharacterDetail.value = true
}
}
@ -85,6 +87,12 @@ const handleBreakthrough = async () => {
showBreakthroughMessage.value = false
}, 3000)
}
const showCharacterDetail = ref(false)
const closeCharacterDetail = () => {
showCharacterDetail.value = false
}
</script>
<template>
@ -170,6 +178,87 @@ const handleBreakthrough = async () => {
<ChatBox />
</div>
<div v-if="showCharacterDetail" class="character-detail-overlay" @click="closeCharacterDetail">
<div class="character-detail-dialog" @click.stop>
<div class="detail-header">
<span class="detail-title">角色详情</span>
<span class="detail-close" @click="closeCharacterDetail">×</span>
</div>
<div class="detail-section">
<div class="section-title">基础信息</div>
<div class="info-row">
<span class="info-label">角色名</span>
<span class="info-value">{{ characterStore.currentCharacter?.name }}</span>
</div>
<div class="info-row">
<span class="info-label">职业</span>
<span class="info-value">{{ characterStore.currentCharacter?.professionName || '未选择' }}</span>
</div>
<div class="info-row">
<span class="info-label">境界</span>
<span class="info-value">{{ characterStore.currentCharacter?.levelName }}</span>
</div>
<div class="info-row">
<span class="info-label">灵石</span>
<span class="info-value">{{ formatNumber(characterStore.currentCharacter?.money || 0) }}</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">属性</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon hp-icon"></span>生命</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseMaxHP || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusMaxHP || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusMaxHP || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon atk-icon"></span>攻击</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseAttack || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusAttack || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusAttack || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon def-icon">🛡</span>防御</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseDefend || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusDefend || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusDefend || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon crit-icon">💥</span>暴击率</span>
<span class="attr-value">
<span class="base-value">{{ (characterStore.currentCharacter?.baseCriticalRate || 0).toFixed(1) }}%</span>
<span v-if="(characterStore.currentCharacter?.bonusCriticalRate || 0) > 0" class="bonus-value">+{{
(characterStore.currentCharacter?.bonusCriticalRate || 0).toFixed(1) }}%</span>
</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">经验</div>
<div class="exp-row">
<span class="exp-label">当前经验</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.currentExp || 0) }}</span>
</div>
<div v-if="characterStore.currentCharacter?.nextLevelMinExp" class="exp-row">
<span class="exp-label">升级所需</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.nextLevelMinExp || 0) }}</span>
</div>
</div>
<div class="detail-footer">
<button class="switch-character-btn" @click="handleSwitchCharacter">切换角色</button>
</div>
</div>
</div>
</div>
</template>
@ -508,4 +597,175 @@ const handleBreakthrough = async () => {
.logout-button:hover .logout-text {
color: #ffffff;
}
.character-detail-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.character-detail-dialog {
background: rgba(20, 20, 25, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
padding: 20px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-title {
color: #ffffff;
font-size: 1.2rem;
font-weight: 600;
}
.detail-close {
color: #666666;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
}
.detail-close:hover {
color: #ffffff;
}
.detail-section {
margin-bottom: 20px;
}
.section-title {
color: #22c55e;
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 12px;
letter-spacing: 0.05em;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.info-label {
color: #888888;
font-size: 0.85rem;
}
.info-value {
color: #ffffff;
font-size: 0.85rem;
}
.attr-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.attr-label {
color: #888888;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
}
.attr-icon {
font-size: 0.9rem;
}
.hp-icon {
color: #ef4444;
}
.atk-icon {
color: #f97316;
}
.def-icon {
color: #3b82f6;
}
.crit-icon {
color: #eab308;
}
.attr-value {
display: flex;
align-items: baseline;
gap: 4px;
}
.base-value {
color: #ffffff;
font-size: 1rem;
font-weight: 500;
}
.bonus-value {
color: #fbbf24;
font-size: 0.8rem;
}
.exp-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.exp-label {
color: #888888;
font-size: 0.85rem;
}
.exp-value {
color: #cccccc;
font-size: 0.85rem;
}
.detail-footer {
margin-top: 20px;
text-align: center;
}
.switch-character-btn {
padding: 12px 24px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
color: #888888;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.switch-character-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.25);
color: #ffffff;
}
</style>

Loading…
Cancel
Save