diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0447634 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.gitnexus diff --git a/.gitnexusignore b/.gitnexusignore new file mode 100644 index 0000000..9c4e155 --- /dev/null +++ b/.gitnexusignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +build/ +tmp/ +temp/ +.cache/ +.env +bin/ +obj/ \ No newline at end of file diff --git a/Build_God_Admin_Frontend/Frontend/src/api/monster.ts b/Build_God_Admin_Frontend/Frontend/src/api/monster.ts index b3de5db..cfc2568 100644 --- a/Build_God_Admin_Frontend/Frontend/src/api/monster.ts +++ b/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[]; } diff --git a/Build_God_Admin_Frontend/Frontend/src/constants/theme.ts b/Build_God_Admin_Frontend/Frontend/src/constants/theme.ts index e9e31d0..3af7360 100644 --- a/Build_God_Admin_Frontend/Frontend/src/constants/theme.ts +++ b/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 = { diff --git a/Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue b/Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue index ae93603..8d4425f 100644 --- a/Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue +++ b/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({ id: 0, name: '', @@ -50,7 +63,8 @@ const formData = ref({ defense: 0, criticalRate: 0, level: 1, - type: 1 + type: 1, + icon: '' }) const rewardFormData = ref({ @@ -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 () => { + + + + + diff --git a/Build_God_Api/Build_God_Api/Controllers/BattleController.cs b/Build_God_Api/Build_God_Api/Controllers/BattleController.cs new file mode 100644 index 0000000..8d160b0 --- /dev/null +++ b/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> 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); + } + } +} \ No newline at end of file diff --git a/Build_God_Api/Build_God_Api/DB/Monster.cs b/Build_God_Api/Build_God_Api/DB/Monster.cs index 44b286d..04d02aa 100644 --- a/Build_God_Api/Build_God_Api/DB/Monster.cs +++ b/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 diff --git a/Build_God_Api/Build_God_Api/Dto/BattleDtos.cs b/Build_God_Api/Build_God_Api/Dto/BattleDtos.cs new file mode 100644 index 0000000..9d47f0b --- /dev/null +++ b/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 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; } = ""; + } +} \ No newline at end of file diff --git a/Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs b/Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs index d19fb2b..398ac2e 100644 --- a/Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs +++ b/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? 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; } + } } diff --git a/Build_God_Api/Build_God_Api/Program.cs b/Build_God_Api/Build_God_Api/Program.cs index d4bc4c2..cc3d474 100644 --- a/Build_God_Api/Build_God_Api/Program.cs +++ b/Build_God_Api/Build_God_Api/Program.cs @@ -158,6 +158,7 @@ namespace Build_God_Api builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddCors(options => { diff --git a/Build_God_Api/Build_God_Api/Services/BattleService.cs b/Build_God_Api/Build_God_Api/Services/BattleService.cs new file mode 100644 index 0000000..7e7a776 --- /dev/null +++ b/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 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 ChallengeMonster(int characterId, int monsterId) + { + var response = new ChallengeMonsterResponse(); + + try + { + var character = await _db.Queryable().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().Where(m => m.Id == monsterId).FirstAsync(); + if (monster == null) + { + response.Success = false; + response.Message = "怪物不存在"; + return response; + } + + var rewards = await _db.Queryable().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 GrantReward(int characterId, MonsterReward reward) + { + var battleReward = new BattleRewardDto(); + + switch (reward.Type) + { + case RewardType.Pill: + case RewardType.Equipment: + var characterBag = await _db.Queryable() + .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().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().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() + .Where(m => m.CharacterId == characterId && m.Status == DailyMissionStatus.InProgress) + .ToListAsync(); + + foreach (var dailyMission in dailyMissions) + { + var missionProgresses = await _db.Queryable() + .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() + .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(); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Build_God_Game/src/api/monster.ts b/Build_God_Game/src/api/monster.ts new file mode 100644 index 0000000..afdc092 --- /dev/null +++ b/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 => { + return http.get('/monster/all', {}) + }, + + getMonsterById: (id: number): Promise => { + return http.get(`/monster/${id}`) + }, + + challengeMonster: (monsterId: number): Promise => { + return http.post('/battle/challenge', { monsterId }) + } +} \ No newline at end of file diff --git a/Build_God_Game/src/assets/images/monster.svg b/Build_God_Game/src/assets/images/monster.svg new file mode 100644 index 0000000..907ca5d --- /dev/null +++ b/Build_God_Game/src/assets/images/monster.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Build_God_Game/src/router/index.ts b/Build_God_Game/src/router/index.ts index 580690c..cb6b02b 100644 --- a/Build_God_Game/src/router/index.ts +++ b/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', diff --git a/Build_God_Game/src/stores/monster.ts b/Build_God_Game/src/stores/monster.ts new file mode 100644 index 0000000..3f7ce3c --- /dev/null +++ b/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([]) + const currentMonster = ref(null) + const lastBattleResult = ref(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 => { + 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 + } +}) \ No newline at end of file diff --git a/Build_God_Game/src/views/BattleView.vue b/Build_God_Game/src/views/BattleView.vue new file mode 100644 index 0000000..ae0bd93 --- /dev/null +++ b/Build_God_Game/src/views/BattleView.vue @@ -0,0 +1,451 @@ + + + + + \ No newline at end of file diff --git a/Build_God_Game/src/views/GameView.vue b/Build_God_Game/src/views/GameView.vue index e4d84d2..4b3f2ce 100644 --- a/Build_God_Game/src/views/GameView.vue +++ b/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() } diff --git a/Build_God_Game/src/views/MonsterListView.vue b/Build_God_Game/src/views/MonsterListView.vue new file mode 100644 index 0000000..c42f414 --- /dev/null +++ b/Build_God_Game/src/views/MonsterListView.vue @@ -0,0 +1,302 @@ + + + + + \ No newline at end of file