18 changed files with 1301 additions and 3 deletions
@ -0,0 +1 @@ |
|||||
|
.gitnexus |
||||
@ -0,0 +1,9 @@ |
|||||
|
node_modules/ |
||||
|
dist/ |
||||
|
build/ |
||||
|
tmp/ |
||||
|
temp/ |
||||
|
.cache/ |
||||
|
.env |
||||
|
bin/ |
||||
|
obj/ |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System.Linq; |
||||
|
using Build_God_Api.DB; |
||||
|
using Build_God_Api.Dto; |
||||
|
using Build_God_Api.Services; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
|
||||
|
namespace Build_God_Api.Controllers |
||||
|
{ |
||||
|
[ApiController] |
||||
|
[Route("api/god/[controller]")]
|
||||
|
[Authorize] |
||||
|
public class BattleController( |
||||
|
IBattleService battleService, |
||||
|
ICharacterService characterService, |
||||
|
ICurrentUserService currentUserService |
||||
|
) : ControllerBase |
||||
|
{ |
||||
|
private readonly IBattleService _battleService = battleService; |
||||
|
private readonly ICharacterService _characterService = characterService; |
||||
|
private readonly ICurrentUserService _currentUserService = currentUserService; |
||||
|
|
||||
|
[HttpPost("challenge")] |
||||
|
public async Task<ActionResult<ChallengeMonsterResponse>> ChallengeMonster([FromBody] ChallengeMonsterRequest request) |
||||
|
{ |
||||
|
var characters = await _characterService.GetAllCharacters(); |
||||
|
var currentCharacter = characters |
||||
|
.Where(c => c.AccountId == _currentUserService.UserId) |
||||
|
.OrderByDescending(c => c.LastLogin) |
||||
|
.FirstOrDefault(); |
||||
|
|
||||
|
if (currentCharacter == null) |
||||
|
{ |
||||
|
return BadRequest(new { message = "请先创建角色" }); |
||||
|
} |
||||
|
|
||||
|
return await _battleService.ChallengeMonster(currentCharacter.Id, request.MonsterId); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
namespace Build_God_Api.Dto |
||||
|
{ |
||||
|
public class ChallengeMonsterRequest |
||||
|
{ |
||||
|
public int MonsterId { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class ChallengeMonsterResponse |
||||
|
{ |
||||
|
public bool Success { get; set; } |
||||
|
public int MonsterId { get; set; } |
||||
|
public string MonsterName { get; set; } = ""; |
||||
|
public int MonsterHealth { get; set; } |
||||
|
public int MonsterMaxHealth { get; set; } |
||||
|
public int PlayerHealth { get; set; } |
||||
|
public int PlayerMaxHealth { get; set; } |
||||
|
public List<BattleRewardDto> Rewards { get; set; } = new(); |
||||
|
public string Message { get; set; } = ""; |
||||
|
} |
||||
|
|
||||
|
public class BattleRewardDto |
||||
|
{ |
||||
|
public string Type { get; set; } = ""; |
||||
|
public string ItemName { get; set; } = ""; |
||||
|
public int Count { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class BattleLogDto |
||||
|
{ |
||||
|
public int Round { get; set; } |
||||
|
public bool IsPlayerAttack { get; set; } |
||||
|
public int Damage { get; set; } |
||||
|
public bool IsCritical { get; set; } |
||||
|
public string Description { get; set; } = ""; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
using Build_God_Api.DB; |
||||
|
using Build_God_Api.Dto; |
||||
|
using Build_God_Api.Services.Game; |
||||
|
using SqlSugar; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Build_God_Api.Services |
||||
|
{ |
||||
|
public interface IBattleService |
||||
|
{ |
||||
|
Task<ChallengeMonsterResponse> ChallengeMonster(int characterId, int monsterId); |
||||
|
} |
||||
|
|
||||
|
public class BattleService( |
||||
|
ISqlSugarClient db, |
||||
|
ICurrentUserService currentUserService, |
||||
|
IBagService bagService, |
||||
|
ICharacterAttributeCalculateService calculateService |
||||
|
) : IBattleService |
||||
|
{ |
||||
|
private readonly ISqlSugarClient _db = db; |
||||
|
private readonly ICurrentUserService _currentUserService = currentUserService; |
||||
|
private readonly IBagService _bagService = bagService; |
||||
|
private readonly ICharacterAttributeCalculateService _calculateService = calculateService; |
||||
|
|
||||
|
public async Task<ChallengeMonsterResponse> ChallengeMonster(int characterId, int monsterId) |
||||
|
{ |
||||
|
var response = new ChallengeMonsterResponse(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var character = await _db.Queryable<Character>().Where(c => c.Id == characterId).FirstAsync(); |
||||
|
if (character == null) |
||||
|
{ |
||||
|
response.Success = false; |
||||
|
response.Message = "角色不存在"; |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
var attributes = await _calculateService.CalculateAttributesAsync(character); |
||||
|
|
||||
|
var monster = await _db.Queryable<Monster>().Where(m => m.Id == monsterId).FirstAsync(); |
||||
|
if (monster == null) |
||||
|
{ |
||||
|
response.Success = false; |
||||
|
response.Message = "怪物不存在"; |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
var rewards = await _db.Queryable<MonsterReward>().Where(r => r.MonsterId == monsterId).ToListAsync(); |
||||
|
|
||||
|
var playerHealth = (int)character.CurrentHP; |
||||
|
var playerMaxHealth = (int)attributes.MaxHP; |
||||
|
var monsterHealth = monster.Health; |
||||
|
var monsterMaxHealth = monster.Health; |
||||
|
|
||||
|
var playerAttack = (int)attributes.Attack; |
||||
|
var playerDefend = (int)attributes.Defend; |
||||
|
var playerCriticalRate = attributes.CriticalRate; |
||||
|
|
||||
|
var monsterAttack = monster.Attack; |
||||
|
var monsterDefend = monster.Defense; |
||||
|
|
||||
|
var round = 0; |
||||
|
var maxRounds = 100; |
||||
|
|
||||
|
while (playerHealth > 0 && monsterHealth > 0 && round < maxRounds) |
||||
|
{ |
||||
|
round++; |
||||
|
|
||||
|
var playerDamage = CalculateDamage(playerAttack, monsterDefend, playerCriticalRate); |
||||
|
monsterHealth -= playerDamage; |
||||
|
|
||||
|
if (monsterHealth <= 0) break; |
||||
|
|
||||
|
var monsterDamage = CalculateDamage(monsterAttack, playerDefend, monster.CriticalRate); |
||||
|
playerHealth -= monsterDamage; |
||||
|
} |
||||
|
|
||||
|
response.MonsterId = monsterId; |
||||
|
response.MonsterName = monster.Name; |
||||
|
response.MonsterHealth = Math.Max(0, monsterHealth); |
||||
|
response.MonsterMaxHealth = monsterMaxHealth; |
||||
|
response.PlayerHealth = Math.Max(0, playerHealth); |
||||
|
response.PlayerMaxHealth = playerMaxHealth; |
||||
|
|
||||
|
if (monsterHealth <= 0) |
||||
|
{ |
||||
|
response.Success = true; |
||||
|
response.Message = "战斗胜利!"; |
||||
|
|
||||
|
foreach (var reward in rewards) |
||||
|
{ |
||||
|
var battleReward = await GrantReward(characterId, reward); |
||||
|
if (battleReward != null) |
||||
|
{ |
||||
|
response.Rewards.Add(battleReward); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await UpdateMissionProgress(characterId, monsterId); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
response.Success = false; |
||||
|
response.Message = "战斗失败"; |
||||
|
} |
||||
|
|
||||
|
character.CurrentHP = Math.Max(0, playerHealth); |
||||
|
await _db.Updateable(character).ExecuteCommandAsync(); |
||||
|
|
||||
|
return response; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Console.WriteLine($"ChallengeMonster error: {ex.Message}"); |
||||
|
response.Success = false; |
||||
|
response.Message = "战斗发生错误"; |
||||
|
return response; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int CalculateDamage(int attack, int defend, decimal criticalRate) |
||||
|
{ |
||||
|
var baseDamage = Math.Max(1, attack - defend); |
||||
|
|
||||
|
var random = Random.Shared.NextDouble(); |
||||
|
var isCritical = random < (double)criticalRate / 100; |
||||
|
|
||||
|
if (isCritical) |
||||
|
{ |
||||
|
baseDamage = (int)(baseDamage * 1.5); |
||||
|
} |
||||
|
|
||||
|
var variance = Random.Shared.Next(90, 111) / 100m; |
||||
|
return (int)(baseDamage * variance); |
||||
|
} |
||||
|
|
||||
|
private async Task<BattleRewardDto?> GrantReward(int characterId, MonsterReward reward) |
||||
|
{ |
||||
|
var battleReward = new BattleRewardDto(); |
||||
|
|
||||
|
switch (reward.Type) |
||||
|
{ |
||||
|
case RewardType.Pill: |
||||
|
case RewardType.Equipment: |
||||
|
var characterBag = await _db.Queryable<CharacterBag>() |
||||
|
.Where(b => b.CharacterId == characterId) |
||||
|
.FirstAsync(); |
||||
|
if (characterBag != null) |
||||
|
{ |
||||
|
await _bagService.AddItemToBag(characterBag.Id, (int)reward.Type, reward.ItemId, reward.Count); |
||||
|
} |
||||
|
battleReward.Type = reward.Type == RewardType.Pill ? "丹药" : "装备"; |
||||
|
battleReward.ItemName = reward.ItemName; |
||||
|
battleReward.Count = reward.Count; |
||||
|
break; |
||||
|
|
||||
|
case RewardType.Money: |
||||
|
await AddMoney(characterId, reward.Count); |
||||
|
battleReward.Type = "灵石"; |
||||
|
battleReward.ItemName = "灵石"; |
||||
|
battleReward.Count = reward.Count; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 处理经验奖励(作为特殊处理:如果ItemName包含"经验")
|
||||
|
if (reward.Type == RewardType.Money && reward.ItemName == "经验") |
||||
|
{ |
||||
|
await AddExperience(characterId, reward.Count); |
||||
|
battleReward.Type = "经验"; |
||||
|
battleReward.ItemName = "经验"; |
||||
|
battleReward.Count = reward.Count; |
||||
|
} |
||||
|
|
||||
|
return battleReward; |
||||
|
} |
||||
|
|
||||
|
private async Task AddExperience(int characterId, int amount) |
||||
|
{ |
||||
|
var character = await _db.Queryable<Character>().Where(c => c.Id == characterId).FirstAsync(); |
||||
|
if (character != null) |
||||
|
{ |
||||
|
character.CurrentExp += amount; |
||||
|
await _db.Updateable(character).ExecuteCommandAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task AddMoney(int characterId, int amount) |
||||
|
{ |
||||
|
var character = await _db.Queryable<Character>().Where(c => c.Id == characterId).FirstAsync(); |
||||
|
if (character != null) |
||||
|
{ |
||||
|
character.Money += amount; |
||||
|
await _db.Updateable(character).ExecuteCommandAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UpdateMissionProgress(int characterId, int monsterId) |
||||
|
{ |
||||
|
var dailyMissions = await _db.Queryable<CharacterDailyMission>() |
||||
|
.Where(m => m.CharacterId == characterId && m.Status == DailyMissionStatus.InProgress) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
foreach (var dailyMission in dailyMissions) |
||||
|
{ |
||||
|
var missionProgresses = await _db.Queryable<MissionProgress>() |
||||
|
.Where(p => p.MissionId == dailyMission.MissionId && p.TargetType == ProgressTargetType.KillMonster) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
foreach (var progress in missionProgresses) |
||||
|
{ |
||||
|
if (progress.TargetItemId == monsterId) |
||||
|
{ |
||||
|
var characterProgress = await _db.Queryable<CharacterMissionProgress>() |
||||
|
.Where(cp => cp.CharacterId == characterId |
||||
|
&& cp.MissionId == dailyMission.MissionId |
||||
|
&& cp.MissionProgressId == progress.Id) |
||||
|
.FirstAsync(); |
||||
|
|
||||
|
if (characterProgress == null) |
||||
|
{ |
||||
|
characterProgress = new CharacterMissionProgress |
||||
|
{ |
||||
|
CharacterId = characterId, |
||||
|
MissionId = dailyMission.MissionId, |
||||
|
MissionProgressId = progress.Id, |
||||
|
CurrentCount = 1, |
||||
|
IsCompleted = 1 >= progress.TargetCount, |
||||
|
CreatedOn = DateTime.Now, |
||||
|
UpdatedOn = DateTime.Now |
||||
|
}; |
||||
|
await _db.Insertable(characterProgress).ExecuteCommandAsync(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
characterProgress.CurrentCount++; |
||||
|
characterProgress.IsCompleted = characterProgress.CurrentCount >= progress.TargetCount; |
||||
|
characterProgress.UpdatedOn = DateTime.Now; |
||||
|
await _db.Updateable(characterProgress).ExecuteCommandAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
import http from './index' |
||||
|
|
||||
|
export interface MonsterDto { |
||||
|
id: number |
||||
|
name: string |
||||
|
description: string |
||||
|
health: number |
||||
|
attack: number |
||||
|
defense: number |
||||
|
criticalRate: number |
||||
|
level: number |
||||
|
type: number |
||||
|
icon?: string |
||||
|
rewards?: MonsterRewardDto[] |
||||
|
} |
||||
|
|
||||
|
export interface MonsterRewardDto { |
||||
|
id: number |
||||
|
monsterId: number |
||||
|
rewardType: number |
||||
|
itemId: number |
||||
|
itemName: string |
||||
|
count: number |
||||
|
} |
||||
|
|
||||
|
export interface ChallengeMonsterResponse { |
||||
|
success: boolean |
||||
|
monsterId: number |
||||
|
monsterName: string |
||||
|
monsterHealth: number |
||||
|
monsterMaxHealth: number |
||||
|
playerHealth: number |
||||
|
playerMaxHealth: number |
||||
|
rewards: BattleRewardDto[] |
||||
|
message: string |
||||
|
} |
||||
|
|
||||
|
export interface BattleRewardDto { |
||||
|
type: string |
||||
|
itemName: string |
||||
|
count: number |
||||
|
} |
||||
|
|
||||
|
export const monsterApi = { |
||||
|
getMonsterList: (): Promise<MonsterDto[]> => { |
||||
|
return http.get('/monster/all', {}) |
||||
|
}, |
||||
|
|
||||
|
getMonsterById: (id: number): Promise<MonsterDto> => { |
||||
|
return http.get(`/monster/${id}`) |
||||
|
}, |
||||
|
|
||||
|
challengeMonster: (monsterId: number): Promise<ChallengeMonsterResponse> => { |
||||
|
return http.post('/battle/challenge', { monsterId }) |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 760 B |
@ -0,0 +1,66 @@ |
|||||
|
import { defineStore } from 'pinia' |
||||
|
import { ref } from 'vue' |
||||
|
import { monsterApi, type MonsterDto, type ChallengeMonsterResponse } from '@/api/monster' |
||||
|
|
||||
|
export const useMonsterStore = defineStore('monster', () => { |
||||
|
const monsters = ref<MonsterDto[]>([]) |
||||
|
const currentMonster = ref<MonsterDto | null>(null) |
||||
|
const lastBattleResult = ref<ChallengeMonsterResponse | null>(null) |
||||
|
const loading = ref(false) |
||||
|
|
||||
|
const fetchMonsters = async () => { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const data = await monsterApi.getMonsterList() |
||||
|
monsters.value = data |
||||
|
} catch (error) { |
||||
|
console.error('Failed to fetch monsters:', error) |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fetchMonster = async (id: number) => { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const data = await monsterApi.getMonsterById(id) |
||||
|
currentMonster.value = data |
||||
|
return data |
||||
|
} catch (error) { |
||||
|
console.error('Failed to fetch monster:', error) |
||||
|
return null |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const challengeMonster = async (monsterId: number): Promise<ChallengeMonsterResponse | null> => { |
||||
|
loading.value = true |
||||
|
try { |
||||
|
const result = await monsterApi.challengeMonster(monsterId) |
||||
|
lastBattleResult.value = result |
||||
|
return result |
||||
|
} catch (error) { |
||||
|
console.error('Failed to challenge monster:', error) |
||||
|
return null |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const clearBattleResult = () => { |
||||
|
lastBattleResult.value = null |
||||
|
currentMonster.value = null |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
monsters, |
||||
|
currentMonster, |
||||
|
lastBattleResult, |
||||
|
loading, |
||||
|
fetchMonsters, |
||||
|
fetchMonster, |
||||
|
challengeMonster, |
||||
|
clearBattleResult |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,451 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { useMonsterStore } from '@/stores/monster' |
||||
|
import { useCharacterStore } from '@/stores/character' |
||||
|
import { ElMessage } from 'element-plus' |
||||
|
import { ArrowLeft } from '@element-plus/icons-vue' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const monsterStore = useMonsterStore() |
||||
|
const characterStore = useCharacterStore() |
||||
|
|
||||
|
const monsterId = computed(() => parseInt(route.query.id as string)) |
||||
|
|
||||
|
const playerHealth = ref(0) |
||||
|
const playerMaxHealth = ref(0) |
||||
|
const monsterHealth = ref(0) |
||||
|
const monsterMaxHealth = ref(0) |
||||
|
const battleLog = ref<{ round: number; text: string; isPlayer: boolean }[]>([]) |
||||
|
const isFighting = ref(false) |
||||
|
const battleResult = ref<'win' | 'lose' | null>(null) |
||||
|
const rewards = ref<{ type: string; itemName: string; count: number }[]>([]) |
||||
|
|
||||
|
const playerHealthPercent = computed(() => { |
||||
|
if (playerMaxHealth.value === 0) return 0 |
||||
|
return (playerHealth.value / playerMaxHealth.value) * 100 |
||||
|
}) |
||||
|
|
||||
|
const monsterHealthPercent = computed(() => { |
||||
|
if (monsterMaxHealth.value === 0) return 0 |
||||
|
return (monsterHealth.value / monsterMaxHealth.value) * 100 |
||||
|
}) |
||||
|
|
||||
|
const currentCharacter = computed(() => characterStore.currentCharacter) |
||||
|
const currentMonster = computed(() => monsterStore.currentMonster) |
||||
|
|
||||
|
const getMonsterIcon = (iconName?: string) => { |
||||
|
if (!iconName) return '/src/assets/images/item-default.svg' |
||||
|
return `/src/assets/images/monster/${iconName}` |
||||
|
} |
||||
|
|
||||
|
const calculateDamage = (attack: number, defend: number, criticalRate: number): { damage: number; isCritical: boolean } => { |
||||
|
const baseDamage = Math.max(1, attack - defend) |
||||
|
const random = Math.random() |
||||
|
const isCritical = random < criticalRate / 100 |
||||
|
const damage = isCritical ? Math.floor(baseDamage * 1.5) : baseDamage |
||||
|
const variance = (90 + Math.floor(Math.random() * 21)) / 100 |
||||
|
return { damage: Math.floor(damage * variance), isCritical } |
||||
|
} |
||||
|
|
||||
|
const startBattle = async () => { |
||||
|
if (!currentMonster.value || !currentCharacter.value) { |
||||
|
ElMessage.error('数据加载中...') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
isFighting.value = true |
||||
|
battleResult.value = null |
||||
|
battleLog.value = [] |
||||
|
rewards.value = [] |
||||
|
|
||||
|
playerMaxHealth.value = currentCharacter.value.maxHP |
||||
|
playerHealth.value = currentCharacter.value.currentHP |
||||
|
monsterMaxHealth.value = currentMonster.value.health |
||||
|
monsterHealth.value = currentMonster.value.health |
||||
|
|
||||
|
const playerAttack = currentCharacter.value.attack |
||||
|
const playerDefend = currentCharacter.value.defend |
||||
|
const playerCriticalRate = currentCharacter.value.criticalRate |
||||
|
const monsterAttack = currentMonster.value.attack |
||||
|
const monsterDefend = currentMonster.value.defense |
||||
|
|
||||
|
let round = 0 |
||||
|
const maxRounds = 100 |
||||
|
|
||||
|
const fight = () => { |
||||
|
if (battleResult.value !== null) return |
||||
|
|
||||
|
round++ |
||||
|
|
||||
|
// 玩家攻击 |
||||
|
const playerResult = calculateDamage(playerAttack, monsterDefend, playerCriticalRate) |
||||
|
monsterHealth.value = Math.max(0, monsterHealth.value - playerResult.damage) |
||||
|
battleLog.value.push({ |
||||
|
round, |
||||
|
text: `你${playerResult.isCritical ? '暴击' : '攻击'}造成 ${playerResult.damage} 伤害`, |
||||
|
isPlayer: true |
||||
|
}) |
||||
|
|
||||
|
if (monsterHealth.value <= 0) { |
||||
|
battleResult.value = 'win' |
||||
|
finishBattle() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 怪物攻击 |
||||
|
const monsterResult = calculateDamage(monsterAttack, playerDefend, currentMonster.value.criticalRate) |
||||
|
playerHealth.value = Math.max(0, playerHealth.value - monsterResult.damage) |
||||
|
battleLog.value.push({ |
||||
|
round, |
||||
|
text: `怪物攻击造成 ${monsterResult.damage} 伤害`, |
||||
|
isPlayer: false |
||||
|
}) |
||||
|
|
||||
|
if (playerHealth.value <= 0) { |
||||
|
battleResult.value = 'lose' |
||||
|
finishBattle() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (round < maxRounds) { |
||||
|
setTimeout(fight, 800) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setTimeout(fight, 500) |
||||
|
} |
||||
|
|
||||
|
const finishBattle = async () => { |
||||
|
if (!currentMonster.value) return |
||||
|
|
||||
|
const result = await monsterStore.challengeMonster(currentMonster.value.id) |
||||
|
|
||||
|
if (result) { |
||||
|
rewards.value = result.rewards || [] |
||||
|
|
||||
|
if (result.success) { |
||||
|
ElMessage.success('战斗胜利!') |
||||
|
} else { |
||||
|
ElMessage.warning('战斗失败') |
||||
|
} |
||||
|
|
||||
|
// 刷新角色数据 |
||||
|
await characterStore.fetchCharacters() |
||||
|
} |
||||
|
|
||||
|
isFighting.value = false |
||||
|
} |
||||
|
|
||||
|
const goBack = () => { |
||||
|
router.push('/monster-list') |
||||
|
} |
||||
|
|
||||
|
const retry = () => { |
||||
|
battleResult.value = null |
||||
|
battleLog.value = [] |
||||
|
rewards.value = [] |
||||
|
startBattle() |
||||
|
} |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
if (monsterId.value) { |
||||
|
await monsterStore.fetchMonster(monsterId.value) |
||||
|
|
||||
|
if (currentCharacter.value) { |
||||
|
playerMaxHealth.value = currentCharacter.value.maxHP |
||||
|
playerHealth.value = currentCharacter.value.currentHP |
||||
|
} |
||||
|
|
||||
|
if (currentMonster.value) { |
||||
|
monsterMaxHealth.value = currentMonster.value.health |
||||
|
monsterHealth.value = currentMonster.value.health |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
monsterStore.clearBattleResult() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="battle-container"> |
||||
|
<div class="battle-header"> |
||||
|
<el-button :icon="ArrowLeft" circle @click="goBack" /> |
||||
|
<h2>战斗</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="!currentMonster || !currentCharacter" class="loading"> |
||||
|
加载中... |
||||
|
</div> |
||||
|
|
||||
|
<template v-else> |
||||
|
<!-- 战斗区域 --> |
||||
|
<div class="battle-area"> |
||||
|
<!-- 怪物 --> |
||||
|
<div class="combatant monster"> |
||||
|
<div class="combatant-icon"> |
||||
|
<img :src="getMonsterIcon(currentMonster.icon)" :alt="currentMonster.name" /> |
||||
|
</div> |
||||
|
<div class="combatant-name">{{ currentMonster.name }}</div> |
||||
|
<el-progress |
||||
|
:percentage="monsterHealthPercent" |
||||
|
:stroke-width="12" |
||||
|
:color="monsterHealthPercent > 30 ? '#ef4444' : '#dc2626'" |
||||
|
/> |
||||
|
<div class="health-text"> |
||||
|
{{ monsterHealth }} / {{ monsterMaxHealth }} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- VS --> |
||||
|
<div class="vs">VS</div> |
||||
|
|
||||
|
<!-- 玩家 --> |
||||
|
<div class="combatant player"> |
||||
|
<div class="combatant-icon"> |
||||
|
<img src="/src/assets/images/character.svg" :alt="currentCharacter.name" /> |
||||
|
</div> |
||||
|
<div class="combatant-name">{{ currentCharacter.name }}</div> |
||||
|
<el-progress |
||||
|
:percentage="playerHealthPercent" |
||||
|
:stroke-width="12" |
||||
|
:color="playerHealthPercent > 30 ? '#22c55e' : '#ef4444'" |
||||
|
/> |
||||
|
<div class="health-text"> |
||||
|
{{ playerHealth }} / {{ playerMaxHealth }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 战斗日志 --> |
||||
|
<div class="battle-log"> |
||||
|
<div |
||||
|
v-for="(log, index) in battleLog" |
||||
|
:key="index" |
||||
|
class="log-item" |
||||
|
:class="{ player: log.isPlayer }" |
||||
|
> |
||||
|
<span class="round">第{{ log.round }}回合</span> |
||||
|
<span class="text">{{ log.text }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 结果区域 --> |
||||
|
<div v-if="battleResult" class="battle-result"> |
||||
|
<div class="result-title" :class="{ win: battleResult === 'win', lose: battleResult === 'lose' }"> |
||||
|
{{ battleResult === 'win' ? '战斗胜利!' : '战斗失败' }} |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="battleResult === 'win' && rewards.length > 0" class="rewards"> |
||||
|
<h3>获得奖励</h3> |
||||
|
<div class="reward-list"> |
||||
|
<div v-for="reward in rewards" :key="reward.itemName" class="reward-item"> |
||||
|
<span class="reward-type">{{ reward.type }}</span> |
||||
|
<span class="reward-name">{{ reward.itemName }}</span> |
||||
|
<span class="reward-count">×{{ reward.count }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="result-actions"> |
||||
|
<el-button @click="goBack">返回</el-button> |
||||
|
<el-button type="primary" @click="retry">再战</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 开始战斗按钮 --> |
||||
|
<div v-if="!battleResult && !isFighting" class="start-battle"> |
||||
|
<el-button type="danger" size="large" @click="startBattle"> |
||||
|
开始战斗 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="css"> |
||||
|
.battle-container { |
||||
|
padding: 20px; |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%); |
||||
|
} |
||||
|
|
||||
|
.battle-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16px; |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.battle-header h2 { |
||||
|
color: #f8fafc; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.loading { |
||||
|
color: #64748b; |
||||
|
text-align: center; |
||||
|
padding: 60px 20px; |
||||
|
} |
||||
|
|
||||
|
.battle-area { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
padding: 40px 20px; |
||||
|
background: rgba(0, 0, 0, 0.2); |
||||
|
border-radius: 16px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.combatant { |
||||
|
text-align: center; |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.combatant-icon { |
||||
|
width: 100px; |
||||
|
height: 100px; |
||||
|
margin: 0 auto 16px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, 0.08); |
||||
|
border-radius: 16px; |
||||
|
} |
||||
|
|
||||
|
.combatant-icon img { |
||||
|
width: 70px; |
||||
|
height: 70px; |
||||
|
object-fit: contain; |
||||
|
} |
||||
|
|
||||
|
.combatant-name { |
||||
|
color: #f8fafc; |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.health-text { |
||||
|
color: #94a3b8; |
||||
|
font-size: 14px; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.vs { |
||||
|
color: #f59e0b; |
||||
|
font-size: 32px; |
||||
|
font-weight: 800; |
||||
|
padding: 0 24px; |
||||
|
} |
||||
|
|
||||
|
.battle-log { |
||||
|
background: rgba(0, 0, 0, 0.3); |
||||
|
border-radius: 12px; |
||||
|
padding: 16px; |
||||
|
max-height: 200px; |
||||
|
overflow-y: auto; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.log-item { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
padding: 8px 0; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04); |
||||
|
} |
||||
|
|
||||
|
.log-item:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.log-item .round { |
||||
|
color: #64748b; |
||||
|
font-size: 12px; |
||||
|
min-width: 70px; |
||||
|
} |
||||
|
|
||||
|
.log-item .text { |
||||
|
color: #f8fafc; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.log-item.player .text { |
||||
|
color: #22c55e; |
||||
|
} |
||||
|
|
||||
|
.battle-result { |
||||
|
text-align: center; |
||||
|
padding: 24px; |
||||
|
background: rgba(0, 0, 0, 0.3); |
||||
|
border-radius: 12px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.result-title { |
||||
|
font-size: 28px; |
||||
|
font-weight: 700; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.result-title.win { |
||||
|
color: #22c55e; |
||||
|
} |
||||
|
|
||||
|
.result-title.lose { |
||||
|
color: #ef4444; |
||||
|
} |
||||
|
|
||||
|
.rewards { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.rewards h3 { |
||||
|
color: #f8fafc; |
||||
|
margin: 0 0 12px; |
||||
|
} |
||||
|
|
||||
|
.reward-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.reward-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 8px; |
||||
|
padding: 12px; |
||||
|
background: rgba(34, 197, 94, 0.1); |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.reward-type { |
||||
|
color: #22c55e; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.reward-name { |
||||
|
color: #f8fafc; |
||||
|
} |
||||
|
|
||||
|
.reward-count { |
||||
|
color: #22c55e; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.result-actions { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.start-battle { |
||||
|
text-align: center; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,302 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, onMounted } from 'vue' |
||||
|
import { useRouter } from 'vue-router' |
||||
|
import { useMonsterStore } from '@/stores/monster' |
||||
|
import { useCharacterStore } from '@/stores/character' |
||||
|
import { ElMessage } from 'element-plus' |
||||
|
import defaultIcon from '@/assets/images/item-default.svg' |
||||
|
|
||||
|
const router = useRouter() |
||||
|
const monsterStore = useMonsterStore() |
||||
|
const characterStore = useCharacterStore() |
||||
|
|
||||
|
const getMonsterIcon = (iconName?: string) => { |
||||
|
if (!iconName) return defaultIcon |
||||
|
return `/src/assets/images/monster/${iconName}` |
||||
|
} |
||||
|
|
||||
|
const getMonsterTypeLabel = (type: number) => { |
||||
|
const labels: Record<number, string> = { |
||||
|
1: '普通', |
||||
|
2: '精英', |
||||
|
3: '首领' |
||||
|
} |
||||
|
return labels[type] || '普通' |
||||
|
} |
||||
|
|
||||
|
const getMonsterTypeClass = (type: number) => { |
||||
|
const classes: Record<number, string> = { |
||||
|
1: 'type-normal', |
||||
|
2: 'type-elite', |
||||
|
3: 'type-boss' |
||||
|
} |
||||
|
return classes[type] || 'type-normal' |
||||
|
} |
||||
|
|
||||
|
const playerLevel = computed(() => characterStore.currentCharacter?.levelId || 0) |
||||
|
|
||||
|
const canChallenge = (monster: { level: number }) => { |
||||
|
return playerLevel.value >= monster.level |
||||
|
} |
||||
|
|
||||
|
const handleChallenge = async (monster: { id: number; level: number }) => { |
||||
|
if (!canChallenge(monster)) { |
||||
|
ElMessage.warning('等级不足,无法挑战') |
||||
|
return |
||||
|
} |
||||
|
router.push(`/battle?id=${monster.id}`) |
||||
|
} |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
await monsterStore.fetchMonsters() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="monster-list-container"> |
||||
|
<div class="header"> |
||||
|
<h2>怪物挑战</h2> |
||||
|
<span class="player-level">等级: {{ playerLevel }}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="monsterStore.loading" class="loading"> |
||||
|
加载中... |
||||
|
</div> |
||||
|
|
||||
|
<div v-else-if="monsterStore.monsters.length === 0" class="empty"> |
||||
|
暂无可挑战的怪物 |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="monster-grid"> |
||||
|
<div |
||||
|
v-for="monster in monsterStore.monsters" |
||||
|
:key="monster.id" |
||||
|
class="monster-card" |
||||
|
:class="{ disabled: !canChallenge(monster) }" |
||||
|
@click="handleChallenge(monster)" |
||||
|
> |
||||
|
<div class="monster-icon"> |
||||
|
<img :src="getMonsterIcon(monster.icon)" :alt="monster.name" /> |
||||
|
</div> |
||||
|
<div class="monster-info"> |
||||
|
<h3>{{ monster.name }}</h3> |
||||
|
<span class="monster-level">等级: {{ monster.level }}</span> |
||||
|
<span class="monster-type" :class="getMonsterTypeClass(monster.type)"> |
||||
|
{{ getMonsterTypeLabel(monster.type) }} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="monster-stats"> |
||||
|
<div class="stat"> |
||||
|
<span class="label">生命</span> |
||||
|
<span class="value">{{ monster.health }}</span> |
||||
|
</div> |
||||
|
<div class="stat"> |
||||
|
<span class="label">攻击</span> |
||||
|
<span class="value">{{ monster.attack }}</span> |
||||
|
</div> |
||||
|
<div class="stat"> |
||||
|
<span class="label">防御</span> |
||||
|
<span class="value">{{ monster.defense }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="monster-rewards" v-if="monster.rewards && monster.rewards.length > 0"> |
||||
|
<span class="reward-label">击杀奖励:</span> |
||||
|
<span |
||||
|
v-for="reward in monster.rewards" |
||||
|
:key="reward.id" |
||||
|
class="reward-item" |
||||
|
> |
||||
|
{{ reward.itemName }} ×{{ reward.count }} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="challenge-hint"> |
||||
|
{{ canChallenge(monster) ? '点击挑战' : '等级不足' }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="css"> |
||||
|
.monster-list-container { |
||||
|
padding: 20px; |
||||
|
min-height: 100vh; |
||||
|
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%); |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.header h2 { |
||||
|
color: #f8fafc; |
||||
|
margin: 0; |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
|
||||
|
.player-level { |
||||
|
color: #94a3b8; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.loading, |
||||
|
.empty { |
||||
|
color: #64748b; |
||||
|
text-align: center; |
||||
|
padding: 60px 20px; |
||||
|
} |
||||
|
|
||||
|
.monster-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.monster-card { |
||||
|
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); |
||||
|
border: 1px solid rgba(255, 255, 255, 0.08); |
||||
|
border-radius: 12px; |
||||
|
padding: 20px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
} |
||||
|
|
||||
|
.monster-card:hover { |
||||
|
transform: translateY(-2px); |
||||
|
border-color: rgba(139, 92, 246, 0.5); |
||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); |
||||
|
} |
||||
|
|
||||
|
.monster-card.disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.monster-card.disabled:hover { |
||||
|
transform: none; |
||||
|
border-color: rgba(255, 255, 255, 0.08); |
||||
|
box-shadow: none; |
||||
|
} |
||||
|
|
||||
|
.monster-icon { |
||||
|
width: 80px; |
||||
|
height: 80px; |
||||
|
margin: 0 auto 16px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, 0.04); |
||||
|
border-radius: 12px; |
||||
|
} |
||||
|
|
||||
|
.monster-icon img { |
||||
|
width: 60px; |
||||
|
height: 60px; |
||||
|
object-fit: contain; |
||||
|
} |
||||
|
|
||||
|
.monster-info { |
||||
|
text-align: center; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.monster-info h3 { |
||||
|
color: #f8fafc; |
||||
|
margin: 0 0 8px; |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
|
||||
|
.monster-level { |
||||
|
color: #94a3b8; |
||||
|
font-size: 14px; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.monster-type { |
||||
|
display: inline-block; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.type-normal { |
||||
|
background-color: #475569; |
||||
|
color: #e2e8f0; |
||||
|
} |
||||
|
|
||||
|
.type-elite { |
||||
|
background-color: #7c3aed; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.type-boss { |
||||
|
background-color: #dc2626; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.monster-stats { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
gap: 16px; |
||||
|
margin: 12px 0; |
||||
|
padding: 12px 0; |
||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08); |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
||||
|
} |
||||
|
|
||||
|
.stat { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.stat .label { |
||||
|
display: block; |
||||
|
color: #64748b; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.stat .value { |
||||
|
display: block; |
||||
|
color: #f8fafc; |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.monster-rewards { |
||||
|
margin-top: 12px; |
||||
|
padding-top: 12px; |
||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08); |
||||
|
} |
||||
|
|
||||
|
.reward-label { |
||||
|
display: block; |
||||
|
color: #64748b; |
||||
|
font-size: 12px; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.reward-item { |
||||
|
display: inline-block; |
||||
|
color: #22c55e; |
||||
|
font-size: 12px; |
||||
|
margin-right: 8px; |
||||
|
background: rgba(34, 197, 94, 0.1); |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.challenge-hint { |
||||
|
margin-top: 12px; |
||||
|
text-align: center; |
||||
|
color: #8b5cf6; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.monster-card.disabled .challenge-hint { |
||||
|
color: #ef4444; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue