Browse Source

增加一个怪物的显示界面

master
秦汉 4 weeks ago
parent
commit
b53a0da20e
  1. 1
      .gitignore
  2. 9
      .gitnexusignore
  3. 1
      Build_God_Admin_Frontend/Frontend/src/api/monster.ts
  4. 15
      Build_God_Admin_Frontend/Frontend/src/constants/theme.ts
  5. 24
      Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue
  6. 40
      Build_God_Api/Build_God_Api/Controllers/BattleController.cs
  7. 3
      Build_God_Api/Build_God_Api/DB/Monster.cs
  8. 36
      Build_God_Api/Build_God_Api/Dto/BattleDtos.cs
  9. 25
      Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs
  10. 1
      Build_God_Api/Build_God_Api/Program.cs
  11. 247
      Build_God_Api/Build_God_Api/Services/BattleService.cs
  12. 56
      Build_God_Game/src/api/monster.ts
  13. 11
      Build_God_Game/src/assets/images/monster.svg
  14. 12
      Build_God_Game/src/router/index.ts
  15. 66
      Build_God_Game/src/stores/monster.ts
  16. 451
      Build_God_Game/src/views/BattleView.vue
  17. 4
      Build_God_Game/src/views/GameView.vue
  18. 302
      Build_God_Game/src/views/MonsterListView.vue

1
.gitignore

@ -0,0 +1 @@
.gitnexus

9
.gitnexusignore

@ -0,0 +1,9 @@
node_modules/
dist/
build/
tmp/
temp/
.cache/
.env
bin/
obj/

1
Build_God_Admin_Frontend/Frontend/src/api/monster.ts

@ -10,6 +10,7 @@ export interface Monster {
criticalRate: number;
level: number;
type: number;
icon?: string;
rewards?: MonsterReward[];
}

15
Build_God_Admin_Frontend/Frontend/src/constants/theme.ts

@ -16,7 +16,20 @@ export const ICONS = {
money: '💰',
bag:'👜',
default: '🎁'
}
},
monsterIcons: [
{ value: '', label: '无图标' },
{ value: 'monster-wolf.svg', label: '狼' },
{ value: 'monster-skeleton.svg', label: '骷髅' },
{ value: 'monster-dragon.svg', label: '龙' },
{ value: 'monster-ghost.svg', label: '鬼' },
{ value: 'monster-slime.svg', label: '史莱姆' },
{ value: 'monster-bat.svg', label: '蝙蝠' },
{ value: 'monster-spider.svg', label: '蜘蛛' },
{ value: 'monster-snake.svg', label: '蛇' },
{ value: 'monster-golem.svg', label: '石魔' }
]
}
export const COLORS = {

24
Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue

@ -41,6 +41,19 @@ const isEditing = ref(false)
const isEditingReward = ref(false)
const searchQuery = ref('')
const monsterIconOptions = [
{ value: '', label: '无图标' },
{ value: 'monster-wolf.svg', label: '狼' },
{ value: 'monster-skeleton.svg', label: '骷髅' },
{ value: 'monster-dragon.svg', label: '龙' },
{ value: 'monster-ghost.svg', label: '鬼' },
{ value: 'monster-slime.svg', label: '史莱姆' },
{ value: 'monster-bat.svg', label: '蝙蝠' },
{ value: 'monster-spider.svg', label: '蜘蛛' },
{ value: 'monster-snake.svg', label: '蛇' },
{ value: 'monster-golem.svg', label: '石魔' }
]
const formData = ref<Monster>({
id: 0,
name: '',
@ -50,7 +63,8 @@ const formData = ref<Monster>({
defense: 0,
criticalRate: 0,
level: 1,
type: 1
type: 1,
icon: ''
})
const rewardFormData = ref<MonsterReward>({
@ -103,7 +117,8 @@ const openDialog = (monster?: Monster) => {
defense: 0,
criticalRate: 0,
level: 1,
type: 1
type: 1,
icon: ''
}
}
showDialog.value = true
@ -482,6 +497,11 @@ const fetchRewardTypes = async () => {
<el-form-item label="暴击率(%)">
<el-input-number v-model="formData.criticalRate" :min="0" :max="100" />
</el-form-item>
<el-form-item label="图标">
<el-select v-model="formData.icon" placeholder="选择图标">
<el-option v-for="icon in monsterIconOptions" :key="icon.value" :value="icon.value" :label="icon.label" />
</el-select>
</el-form-item>
<el-form-item label="描述" style="width: 100%;">
<el-input v-model="formData.description" placeholder="描述" type="textarea" style="width: 100%;" />
</el-form-item>

40
Build_God_Api/Build_God_Api/Controllers/BattleController.cs

@ -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);
}
}
}

3
Build_God_Api/Build_God_Api/DB/Monster.cs

@ -22,6 +22,9 @@ namespace Build_God_Api.DB
public int Level { get; set; }
public MonsterType Type { get; set; }
[SugarColumn(Length = 100, IsNullable = true)]
public string? Icon { get; set; }
}
public class MonsterReward : BaseEntity

36
Build_God_Api/Build_God_Api/Dto/BattleDtos.cs

@ -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; } = "";
}
}

25
Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs

@ -7,4 +7,29 @@ namespace Build_God_Api.Dto
public int? MonsterType { get; set; }
public int? Level { get; set; }
}
public class MonsterDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int Health { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }
public decimal CriticalRate { get; set; }
public int Level { get; set; }
public int Type { get; set; }
public string? Icon { get; set; }
public List<MonsterRewardDto>? Rewards { get; set; }
}
public class MonsterRewardDto
{
public int Id { get; set; }
public int MonsterId { get; set; }
public int RewardType { get; set; }
public int ItemId { get; set; }
public string ItemName { get; set; } = string.Empty;
public int Count { get; set; }
}
}

1
Build_God_Api/Build_God_Api/Program.cs

@ -158,6 +158,7 @@ namespace Build_God_Api
builder.Services.AddScoped<IChatService, ChatService>();
builder.Services.AddScoped<IScrapService, ScrapService>();
builder.Services.AddScoped<IMonsterService, MonsterService>();
builder.Services.AddScoped<IBattleService, BattleService>();
builder.Services.AddCors(options =>
{

247
Build_God_Api/Build_God_Api/Services/BattleService.cs

@ -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();
}
}
}
}
}
}
}

56
Build_God_Game/src/api/monster.ts

@ -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 })
}
}

11
Build_God_Game/src/assets/images/monster.svg

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="28" fill="#7c3aed" opacity="0.2"/>
<circle cx="32" cy="32" r="24" fill="#7c3aed" opacity="0.3"/>
<path d="M20 24c-4-4-8-2-8 4s4 8 8 8" stroke="#22c55e" stroke-width="3" stroke-linecap="round"/>
<circle cx="24" cy="26" r="3" fill="#22c55e"/>
<path d="M44 24c4-4 8-2 8 4s-4 8-8 8" stroke="#22c55e" stroke-width="3" stroke-linecap="round"/>
<circle cx="40" cy="26" r="3" fill="#22c55e"/>
<path d="M16 36c-4 4-2 8 4 8s8-4 8-8" stroke="#ef4444" stroke-width="3" stroke-linecap="round"/>
<path d="M48 36c4 4 2 8-4 8s-8-4-8-8" stroke="#ef4444" stroke-width="3" stroke-linecap="round"/>
<ellipse cx="32" cy="44" rx="12" ry="6" fill="#7c3aed"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

12
Build_God_Game/src/router/index.ts

@ -63,6 +63,18 @@ const router = createRouter({
component: () => import('@/views/BagView.vue'),
meta: { requiresAuth: true }
},
{
path: '/monster-list',
name: 'monster-list',
component: () => import('@/views/MonsterListView.vue'),
meta: { requiresAuth: true }
},
{
path: '/battle',
name: 'battle',
component: () => import('@/views/BattleView.vue'),
meta: { requiresAuth: true }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',

66
Build_God_Game/src/stores/monster.ts

@ -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
}
})

451
Build_God_Game/src/views/BattleView.vue

@ -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>

4
Build_God_Game/src/views/GameView.vue

@ -14,6 +14,7 @@ import missionIcon from '@/assets/images/mission.svg'
import scrapIcon from '@/assets/images/scrap.svg'
import characterIco from '@/assets/images/character.svg'
import bagIcon from '@/assets/images/bag.svg'
import monsterIcon from '@/assets/images/monster.svg'
const authStore = useAuthStore()
const characterStore = useCharacterStore()
@ -45,6 +46,7 @@ const menuItems = computed(() => [
{ label: isTraining.value ? '打坐中' : '打坐', icon: trainingIcon, useImage: true, isTraining: isTraining.value },
{ label: '背包', icon: bagIcon, useImage: true },
{ label: '捡垃圾', icon: scrapIcon, useImage: true },
{ label: '挑战', icon: monsterIcon, useImage: true },
])
const formatNumber = (num: number) => {
@ -74,6 +76,8 @@ const navigateTo = (item: { label: string }) => {
router.push('/bag')
} else if (item.label === '捡垃圾') {
router.push('/scrap')
} else if (item.label === '挑战') {
router.push('/monster-list')
} else if (item.label === '角色') {
openCharacterDetail()
}

302
Build_God_Game/src/views/MonsterListView.vue

@ -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…
Cancel
Save