Browse Source

怪物的属性该为计算,而不是固定值,固定值不好配置

master
hanqin 1 week ago
parent
commit
6857fa7f05
  1. 50
      Build_God_Admin_Frontend/Frontend/src/api/monster.ts
  2. 199
      Build_God_Admin_Frontend/Frontend/src/views/admin/MonstersView.vue
  3. 8
      Build_God_Api/Build_God_Api/Controllers/MonsterController.cs
  4. 18
      Build_God_Api/Build_God_Api/DB/Monster.cs
  5. 17
      Build_God_Api/Build_God_Api/DB/MonsterEquipment.cs
  6. 56
      Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs
  7. 2
      Build_God_Api/Build_God_Api/Program.cs
  8. 14
      Build_God_Api/Build_God_Api/Scripts/monster_schema_v2_postgresql.sql
  9. 16
      Build_God_Api/Build_God_Api/Services/BattleService.cs
  10. 78
      Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs
  11. 28
      Build_God_Api/Build_God_Api/Services/Game/CombatAttributeFormula.cs
  12. 88
      Build_God_Api/Build_God_Api/Services/Game/EquipmentAttributeBonus.cs
  13. 65
      Build_God_Api/Build_God_Api/Services/Game/MonsterAttributeCalculateService.cs
  14. 205
      Build_God_Api/Build_God_Api/Services/MonsterService.cs
  15. 18
      Build_God_Game/src/api/monster.ts

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

@ -1,17 +1,34 @@
import http, { type EnumInfoDto } from "@/api/index"; import http, { type EnumInfoDto } from "@/api/index";
export interface MonsterEquipmentRow {
equipmentTemplateId: number;
attributes: string;
}
export interface MonsterEquipmentDto {
id: number;
monsterId: number;
equipmentTemplateId: number;
attributes: string;
equipmentTemplateName?: string;
}
export interface Monster { export interface Monster {
id: number; id: number;
name: string; name: string;
description: string; description: string;
configuredExp: number;
professionId?: number | null;
levelId: number;
type: number;
icon?: string;
/** 计算后的展示属性 */
health: number; health: number;
attack: number; attack: number;
defense: number; defense: number;
criticalRate: number; criticalRate: number;
levelId: number;
type: number;
icon?: string;
rewards?: MonsterReward[]; rewards?: MonsterReward[];
equipment?: MonsterEquipmentDto[];
} }
export interface MonsterReward { export interface MonsterReward {
@ -33,20 +50,32 @@ interface SearchMonsterDto {
pageNumber: number | undefined; pageNumber: number | undefined;
pageSize: number | undefined; pageSize: number | undefined;
monsterType: number | undefined; monsterType: number | undefined;
level: number | undefined; levelId: number | undefined;
}
export interface MonsterSavePayload {
id: number;
name: string;
description: string;
configuredExp: number;
professionId?: number | null;
levelId: number;
type: number;
icon?: string;
equipment: MonsterEquipmentRow[];
} }
export const GetMonsterList = ( export const GetMonsterList = (
monsterType?: number, monsterType?: number,
level?: number, levelId?: number,
pageNumber?: number, pageNumber?: number,
pageSize?: number pageSize?: number
): Promise<PagedResult<Monster> | Monster[]> => { ): Promise<PagedResult<Monster> | Monster[]> => {
var dto: SearchMonsterDto = { const dto: SearchMonsterDto = {
pageNumber: pageNumber, pageNumber: pageNumber,
pageSize: pageSize, pageSize: pageSize,
monsterType: monsterType, monsterType: monsterType,
level: level, levelId: levelId,
}; };
return http.post("/monster/all", dto); return http.post("/monster/all", dto);
}; };
@ -55,11 +84,12 @@ export const GetMonsterById = (id: number): Promise<Monster> => {
return http.get(`/monster/${id}`); return http.get(`/monster/${id}`);
}; };
export const AddMonster = (data: Monster): Promise<boolean> => { export const AddMonster = (data: MonsterSavePayload): Promise<boolean> => {
return http.post("/monster", data); const { id: _id, ...body } = data;
return http.post("/monster", body);
}; };
export const UpdateMonster = (data: Monster): Promise<boolean> => { export const UpdateMonster = (data: MonsterSavePayload): Promise<boolean> => {
return http.put(`/monster/${data.id}`, data); return http.put(`/monster/${data.id}`, data);
}; };

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

@ -9,6 +9,8 @@ import {
UpdateMonster, UpdateMonster,
type Monster, type Monster,
type MonsterReward, type MonsterReward,
type MonsterEquipmentRow,
type MonsterSavePayload,
GetRewardTypes, GetRewardTypes,
AddMonsterReward, AddMonsterReward,
UpdateMonsterReward, UpdateMonsterReward,
@ -21,6 +23,7 @@ import { GetEquipmentTemplateList, type EquipmentTemplate } from '@/api/equipmen
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue' import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
import { ICONS } from '@/constants/theme' import { ICONS } from '@/constants/theme'
import { GetLevelList, type Level } from '@/api/level' import { GetLevelList, type Level } from '@/api/level'
import { GetProfessionList, type Profession } from '@/api/spirit'
const monsters = ref<Monster[]>([]) const monsters = ref<Monster[]>([])
@ -28,8 +31,8 @@ const monsterTypes = ref<EnumInfoDto[]>([])
const rewardTypes = ref<EnumInfoDto[]>([]) const rewardTypes = ref<EnumInfoDto[]>([])
const pillData = ref<Pill[]>([]) const pillData = ref<Pill[]>([])
const equipmentData = ref<EquipmentTemplate[]>([]) const equipmentData = ref<EquipmentTemplate[]>([])
//
const levels = ref<Level[]>([]) const levels = ref<Level[]>([])
const professions = ref<Profession[]>([])
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
@ -59,19 +62,26 @@ const monsterIconOptions = [
{ value: '猪.svg', label: '猪' } { value: '猪.svg', label: '猪' }
] ]
const formData = ref<Monster>({ type MonsterFormData = Omit<Monster, 'equipment'> & { equipment: MonsterEquipmentRow[] }
const defaultMonsterForm = (): MonsterFormData => ({
id: 0, id: 0,
name: '', name: '',
description: '', description: '',
configuredExp: 0,
professionId: undefined,
levelId: 1,
type: 1,
icon: '',
health: 0, health: 0,
attack: 0, attack: 0,
defense: 0, defense: 0,
criticalRate: 0, criticalRate: 0,
levelId: 1, equipment: []
type: 1,
icon: ''
}) })
const formData = ref<MonsterFormData>(defaultMonsterForm())
const rewardFormData = ref<MonsterReward>({ const rewardFormData = ref<MonsterReward>({
id: 0, id: 0,
monsterId: 0, monsterId: 0,
@ -95,8 +105,7 @@ const translateMonsterType = (typeId: number) => {
} }
const translateLevel = (id: number) => { const translateLevel = (id: number) => {
console.log(levels) const item = levels.value.find(x => x.levelId == id)
var item = levels.value.find(x => x.levelId == id);
if (item) { if (item) {
return item.name return item.name
} }
@ -116,28 +125,46 @@ const getMonsterTypeClass = (typeId: number) => {
} }
} }
const openDialog = (monster?: Monster) => { const openDialog = async (monster?: Monster) => {
if (!equipmentData.value.length) {
const eqs = await fetchEquipments()
equipmentData.value = eqs
}
if (!professions.value.length) {
try {
professions.value = await GetProfessionList()
} catch {
professions.value = []
}
}
if (monster !== undefined) { if (monster !== undefined) {
isEditing.value = true isEditing.value = true
formData.value = { ...monster }
} else {
isEditing.value = false
formData.value = { formData.value = {
id: 0, ...monster,
name: '', equipment: (monster.equipment || []).map(e => ({
description: '', equipmentTemplateId: e.equipmentTemplateId,
health: 0, attributes: e.attributes || '[]'
attack: 0, }))
defense: 0,
criticalRate: 0,
levelId: 1,
type: 1,
icon: ''
} }
} else {
isEditing.value = false
formData.value = defaultMonsterForm()
} }
showDialog.value = true showDialog.value = true
} }
const addEquipmentRow = () => {
const firstId = equipmentData.value[0]?.id ?? 0
formData.value.equipment.push({
equipmentTemplateId: firstId,
attributes: '[]'
})
}
const removeEquipmentRow = (index: number) => {
formData.value.equipment.splice(index, 1)
}
const closeDialog = () => { const closeDialog = () => {
showDialog.value = false showDialog.value = false
} }
@ -268,6 +295,21 @@ const formatRewardDisplay = (r: MonsterReward) => {
return r.itemName ? `${r.itemName} ×${r.count}` : `×${r.count}` return r.itemName ? `${r.itemName} ×${r.count}` : `×${r.count}`
} }
const buildSavePayload = (): MonsterSavePayload => ({
id: formData.value.id,
name: formData.value.name,
description: formData.value.description,
configuredExp: formData.value.configuredExp,
professionId: formData.value.professionId ?? null,
levelId: formData.value.levelId,
type: formData.value.type,
icon: formData.value.icon,
equipment: formData.value.equipment.map(e => ({
equipmentTemplateId: e.equipmentTemplateId,
attributes: (e.attributes || '').trim() || '[]'
}))
})
const saveMonster = async () => { const saveMonster = async () => {
if (formData.value.name == undefined if (formData.value.name == undefined
|| formData.value.description == undefined || formData.value.description == undefined
@ -278,8 +320,10 @@ const saveMonster = async () => {
return return
} }
const payload = buildSavePayload()
if (isEditing.value) { if (isEditing.value) {
var result = await UpdateMonster(formData.value) var result = await UpdateMonster(payload)
if (result) { if (result) {
ElMessage.success('怪兽更新成功') ElMessage.success('怪兽更新成功')
closeDialog() closeDialog()
@ -288,7 +332,7 @@ const saveMonster = async () => {
ElMessage.error('怪兽更新失败') ElMessage.error('怪兽更新失败')
} }
} else { } else {
var result = await AddMonster(formData.value) var result = await AddMonster(payload)
if (result) { if (result) {
ElMessage.success('怪兽添加成功') ElMessage.success('怪兽添加成功')
closeDialog() closeDialog()
@ -423,13 +467,14 @@ const fetchLevels = async () => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="名称" prop="name" width="150"></el-table-column> <el-table-column label="名称" prop="name" width="150"></el-table-column>
<el-table-column label="等级" prop="levelId" width="80"> <el-table-column label="等级" prop="levelId" width="100">
<template #default="scoped"> <template #default="scoped">
<span> <span>
{{ translateLevel(scoped.row.levelId) }} {{ translateLevel(scoped.row.levelId) }}
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="配置经验" prop="configuredExp" width="100" />
<el-table-column label="类型" width="100"> <el-table-column label="类型" width="100">
<template #default="scoped"> <template #default="scoped">
<span class="monster-type" :class="getMonsterTypeClass(scoped.row.type)"> <span class="monster-type" :class="getMonsterTypeClass(scoped.row.type)">
@ -440,9 +485,9 @@ const fetchLevels = async () => {
<el-table-column label="生命值" prop="health" width="100"></el-table-column> <el-table-column label="生命值" prop="health" width="100"></el-table-column>
<el-table-column label="攻击力" prop="attack" width="100"></el-table-column> <el-table-column label="攻击力" prop="attack" width="100"></el-table-column>
<el-table-column label="防御力" prop="defense" width="100"></el-table-column> <el-table-column label="防御力" prop="defense" width="100"></el-table-column>
<el-table-column label="暴击率" width="100"> <el-table-column label="暴击率" width="90">
<template #default="scoped"> <template #default="scoped">
{{ scoped.row.criticalRate }}% {{ Number(scoped.row.criticalRate).toFixed(1) }}%
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="描述" prop="description" show-overflow-tooltip></el-table-column> <el-table-column label="描述" prop="description" show-overflow-tooltip></el-table-column>
@ -493,7 +538,7 @@ const fetchLevels = async () => {
<!-- Monster Dialog --> <!-- Monster Dialog -->
<div v-if="showDialog" class="dialog-overlay"> <div v-if="showDialog" class="dialog-overlay">
<el-form :inline="true" :model="formData" class="dialog" label-position="top"> <el-form :inline="true" :model="formData" class="dialog dialog-monster" label-position="top">
<div class="dialog-header"> <div class="dialog-header">
<h3>{{ isEditing ? '编辑怪兽' : '添加怪兽' }}</h3> <h3>{{ isEditing ? '编辑怪兽' : '添加怪兽' }}</h3>
<el-button @click="closeDialog" :icon="Close" circle /> <el-button @click="closeDialog" :icon="Close" circle />
@ -513,23 +558,44 @@ const fetchLevels = async () => {
:label="value.description" /> :label="value.description" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="生命值"> <el-form-item label="配置经验">
<el-input-number v-model="formData.health" :min="0" /> <el-input-number v-model="formData.configuredExp" :min="0" :step="100" />
</el-form-item> </el-form-item>
<el-form-item label="攻击力"> <el-form-item label="职业(可选)">
<el-input-number v-model="formData.attack" :min="0" /> <el-select v-model="formData.professionId" placeholder="无" clearable style="width: 200px">
</el-form-item> <el-option v-for="p in professions" :key="p.id" :value="p.id" :label="p.name" />
<el-form-item label="防御力"> </el-select>
<el-input-number v-model="formData.defense" :min="0" />
</el-form-item>
<el-form-item label="暴击率(%)">
<el-input-number v-model="formData.criticalRate" :min="0" :max="100" />
</el-form-item> </el-form-item>
<el-form-item label="图标"> <el-form-item label="图标">
<el-select v-model="formData.icon" placeholder="选择图标"> <el-select v-model="formData.icon" placeholder="选择图标">
<el-option v-for="icon in monsterIconOptions" :key="icon.value" :value="icon.value" :label="icon.label" /> <el-option v-for="icon in monsterIconOptions" :key="icon.value" :value="icon.value" :label="icon.label" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<div class="computed-stats" v-if="isEditing">
<span class="computed-label">计算后属性保存前为上次列表数值保存后刷新</span>
<span>生命 {{ formData.health }}</span>
<span>攻击 {{ formData.attack }}</span>
<span>防御 {{ formData.defense }}</span>
<span>暴击 {{ Number(formData.criticalRate).toFixed(1) }}%</span>
</div>
<div class="equipment-section">
<div class="equipment-head">
<span class="section-title">装备配置JSON 与角色装备实例 attributes 同格式</span>
<el-button type="primary" size="small" @click="addEquipmentRow">添加装备</el-button>
</div>
<div v-for="(row, idx) in formData.equipment" :key="idx" class="equipment-row">
<el-select v-model="row.equipmentTemplateId" placeholder="模板" filterable style="width: 200px">
<el-option v-for="eq in equipmentData" :key="eq.id" :value="eq.id" :label="eq.name" />
</el-select>
<el-input v-model="row.attributes" type="textarea" :rows="2" placeholder='例: [{"type":1,"value":100}]'
class="equipment-attrs" />
<el-button type="danger" size="small" @click="removeEquipmentRow(idx)">删除</el-button>
</div>
<div v-if="formData.equipment.length === 0" class="hint-muted">无装备则仅为公式基础属性</div>
</div>
<el-form-item label="描述" style="width: 100%;"> <el-form-item label="描述" style="width: 100%;">
<el-input v-model="formData.description" placeholder="描述" type="textarea" style="width: 100%;" /> <el-input v-model="formData.description" placeholder="描述" type="textarea" style="width: 100%;" />
</el-form-item> </el-form-item>
@ -611,6 +677,65 @@ const fetchLevels = async () => {
padding: 10px; padding: 10px;
} }
.dialog-monster {
max-width: 640px;
max-height: 90vh;
overflow-y: auto;
}
.computed-stats {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
padding: 10px 0;
color: #9ca3af;
font-size: 13px;
border-top: 1px solid #374151;
border-bottom: 1px solid #374151;
}
.computed-label {
width: 100%;
color: #6b7280;
font-size: 12px;
}
.equipment-section {
width: 100%;
margin: 12px 0;
}
.equipment-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.section-title {
color: #e5e7eb;
font-size: 13px;
}
.equipment-row {
display: flex;
gap: 8px;
align-items: flex-start;
margin-bottom: 10px;
}
.equipment-attrs {
flex: 1;
min-width: 120px;
}
.hint-muted {
color: #6b7280;
font-size: 12px;
}
.dialog .el-input, .dialog .el-input,
.dialog .el-select, .dialog .el-select,
.dialog .el-input-number, .dialog .el-input-number,

8
Build_God_Api/Build_God_Api/Controllers/MonsterController.cs

@ -15,14 +15,14 @@ namespace Build_God_Api.Controllers
[HttpPost] [HttpPost]
[Authorize(Roles = "admin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] Monster item) public async Task<ActionResult<bool>> Add([FromBody] MonsterSaveDto item)
{ {
return await _service.Add(item); return await _service.Add(item);
} }
[HttpPut("{id}")] [HttpPut("{id}")]
[Authorize(Roles = "admin")] [Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update(int id, [FromBody] Monster item) public async Task<ActionResult<bool>> Update(int id, [FromBody] MonsterSaveDto item)
{ {
item.Id = id; item.Id = id;
return await _service.Update(item); return await _service.Update(item);
@ -37,7 +37,7 @@ namespace Build_God_Api.Controllers
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize] [Authorize]
public async Task<ActionResult<Monster>> GetById(int id) public async Task<ActionResult<MonsterResponseDto>> GetById(int id)
{ {
var monster = await _service.GetById(id); var monster = await _service.GetById(id);
if (monster == null) if (monster == null)
@ -49,7 +49,7 @@ namespace Build_God_Api.Controllers
[HttpPost("all")] [HttpPost("all")]
[Authorize] [Authorize]
public async Task<ActionResult<PagedResult<Monster>>> GetAll([FromBody] SearchMonsterDto dto) public async Task<ActionResult<PagedResult<MonsterResponseDto>>> GetAll([FromBody] SearchMonsterDto dto)
{ {
return await _service.GetAll(dto); return await _service.GetAll(dto);
} }

18
Build_God_Api/Build_God_Api/DB/Monster.cs

@ -11,13 +11,17 @@ namespace Build_God_Api.DB
[SugarColumn(Length = 500)] [SugarColumn(Length = 500)]
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public int Health { get; set; } /// <summary>
/// 用于属性公式的配置经验(对应角色 CurrentExp 在 log(exp+1000) 中的角色,无成长)
public int Attack { get; set; } /// </summary>
[SugarColumn(DefaultValue = "0")]
public int Defense { get; set; } public decimal ConfiguredExp { get; set; }
public decimal CriticalRate { get; set; } /// <summary>
/// 职业倍率,空则按 1 计算
/// </summary>
[SugarColumn(IsNullable = true)]
public int? ProfessionId { get; set; }
[SugarColumn(DefaultValue = "10")] [SugarColumn(DefaultValue = "10")]
public int LevelId { get; set; } public int LevelId { get; set; }

17
Build_God_Api/Build_God_Api/DB/MonsterEquipment.cs

@ -0,0 +1,17 @@
using SqlSugar;
namespace Build_God_Api.DB
{
/// <summary>
/// 怪物装备配置(属性 JSON 与 EquipmentInstance.Attributes 同格式)
/// </summary>
public class MonsterEquipment : BaseEntity
{
public int MonsterId { get; set; }
public int EquipmentTemplateId { get; set; }
[SugarColumn(ColumnDataType = "text")]
public string Attributes { get; set; } = "[]";
}
}

56
Build_God_Api/Build_God_Api/Dto/MonsterDtos.cs

@ -9,28 +9,64 @@ namespace Build_God_Api.Dto
public int? CharacterId { get; set; } public int? CharacterId { get; set; }
} }
public class MonsterDto 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; }
}
public class MonsterEquipmentDto
{
public int Id { get; set; }
public int MonsterId { get; set; }
public int EquipmentTemplateId { get; set; }
public string Attributes { get; set; } = "[]";
public string? EquipmentTemplateName { get; set; }
}
public class MonsterEquipmentInputDto
{
public int EquipmentTemplateId { get; set; }
public string Attributes { get; set; } = "[]";
}
/// <summary>
/// 管理端保存 / 游戏与列表返回(含计算后的展示属性)
/// </summary>
public class MonsterResponseDto
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public decimal ConfiguredExp { get; set; }
public int? ProfessionId { get; set; }
public int LevelId { get; set; }
public int Type { get; set; }
public string? Icon { get; set; }
public int Health { get; set; } public int Health { get; set; }
public int Attack { get; set; } public int Attack { get; set; }
public int Defense { get; set; } public int Defense { get; set; }
public decimal CriticalRate { 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 List<MonsterRewardDto>? Rewards { get; set; }
public List<MonsterEquipmentDto>? Equipment { get; set; }
} }
public class MonsterRewardDto public class MonsterSaveDto
{ {
public int Id { get; set; } public int Id { get; set; }
public int MonsterId { get; set; } public string Name { get; set; } = string.Empty;
public int RewardType { get; set; } public string Description { get; set; } = string.Empty;
public int ItemId { get; set; } public decimal ConfiguredExp { get; set; }
public string ItemName { get; set; } = string.Empty; public int? ProfessionId { get; set; }
public int Count { get; set; } public int LevelId { get; set; }
public int Type { get; set; }
public string? Icon { get; set; }
public List<MonsterEquipmentInputDto> Equipment { get; set; } = [];
} }
} }

2
Build_God_Api/Build_God_Api/Program.cs

@ -97,6 +97,7 @@ namespace Build_God_Api
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterDailyMission)); sqlSugarClient.CodeFirst.InitTables(typeof(CharacterDailyMission));
sqlSugarClient.CodeFirst.InitTables(typeof(Scrap)); sqlSugarClient.CodeFirst.InitTables(typeof(Scrap));
sqlSugarClient.CodeFirst.InitTables(typeof(Monster)); sqlSugarClient.CodeFirst.InitTables(typeof(Monster));
sqlSugarClient.CodeFirst.InitTables(typeof(MonsterEquipment));
sqlSugarClient.CodeFirst.InitTables(typeof(MonsterReward)); sqlSugarClient.CodeFirst.InitTables(typeof(MonsterReward));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShop)); sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShop));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShopPurchaseLog)); sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShopPurchaseLog));
@ -153,6 +154,7 @@ namespace Build_God_Api
builder.Services.AddScoped<IMissionService, MissionService>(); builder.Services.AddScoped<IMissionService, MissionService>();
builder.Services.AddSingleton<ITrainingService, TrainingService>(); builder.Services.AddSingleton<ITrainingService, TrainingService>();
builder.Services.AddSingleton<ICharacterAttributeCalculateService, CharacterAttributeCalculateService>(); builder.Services.AddSingleton<ICharacterAttributeCalculateService, CharacterAttributeCalculateService>();
builder.Services.AddScoped<IMonsterAttributeCalculateService, MonsterAttributeCalculateService>();
builder.Services.AddScoped<IStatisticsService, StatisticsService>(); builder.Services.AddScoped<IStatisticsService, StatisticsService>();
builder.Services.AddScoped<IBagService, BagService>(); builder.Services.AddScoped<IBagService, BagService>();
builder.Services.AddScoped<IShopService, ShopService>(); builder.Services.AddScoped<IShopService, ShopService>();

14
Build_God_Api/Build_God_Api/Scripts/monster_schema_v2_postgresql.sql

@ -0,0 +1,14 @@
-- 怪物属性公式化迁移(SqlSugar CodeFirst 不会删除列,需在 PostgreSQL 上手工执行)
-- 新列 ConfiguredExp、ProfessionId 与表 MonsterEquipment 由应用启动时 CodeFirst 创建。
--
-- 若插入新怪物时报错:null value in column "Health" (或其它旧列) violates not-null constraint,
-- 可先执行 DROP 或放宽约束,例如:
--
-- ALTER TABLE "Monster" DROP COLUMN IF EXISTS "Health";
-- ALTER TABLE "Monster" DROP COLUMN IF EXISTS "Attack";
-- ALTER TABLE "Monster" DROP COLUMN IF EXISTS "Defense";
-- ALTER TABLE "Monster" DROP COLUMN IF EXISTS "CriticalRate";
--
-- 或暂用默认值(再按需 DROP):
-- ALTER TABLE "Monster" ALTER COLUMN "Health" SET DEFAULT 0;
-- (其余列同理)

16
Build_God_Api/Build_God_Api/Services/BattleService.cs

@ -15,13 +15,15 @@ namespace Build_God_Api.Services
ISqlSugarClient db, ISqlSugarClient db,
ICurrentUserService currentUserService, ICurrentUserService currentUserService,
IBagService bagService, IBagService bagService,
ICharacterAttributeCalculateService calculateService ICharacterAttributeCalculateService calculateService,
IMonsterAttributeCalculateService monsterAttributeCalculateService
) : IBattleService ) : IBattleService
{ {
private readonly ISqlSugarClient _db = db; private readonly ISqlSugarClient _db = db;
private readonly ICurrentUserService _currentUserService = currentUserService; private readonly ICurrentUserService _currentUserService = currentUserService;
private readonly IBagService _bagService = bagService; private readonly IBagService _bagService = bagService;
private readonly ICharacterAttributeCalculateService _calculateService = calculateService; private readonly ICharacterAttributeCalculateService _calculateService = calculateService;
private readonly IMonsterAttributeCalculateService _monsterAttributeCalculateService = monsterAttributeCalculateService;
public async Task<ChallengeMonsterResponse> ChallengeMonster(int characterId, int monsterId) public async Task<ChallengeMonsterResponse> ChallengeMonster(int characterId, int monsterId)
{ {
@ -47,19 +49,21 @@ namespace Build_God_Api.Services
return response; return response;
} }
var monsterStats = await _monsterAttributeCalculateService.CalculateAsync(monster);
var rewards = await _db.Queryable<MonsterReward>().Where(r => r.MonsterId == monsterId).ToListAsync(); var rewards = await _db.Queryable<MonsterReward>().Where(r => r.MonsterId == monsterId).ToListAsync();
var playerHealth = (int)character.CurrentHP; var playerHealth = (int)character.CurrentHP;
var playerMaxHealth = (int)attributes.MaxHP; var playerMaxHealth = (int)attributes.MaxHP;
var monsterHealth = monster.Health; var monsterHealth = (int)monsterStats.MaxHP;
var monsterMaxHealth = monster.Health; var monsterMaxHealth = (int)monsterStats.MaxHP;
var playerAttack = (int)attributes.Attack; var playerAttack = (int)attributes.Attack;
var playerDefend = (int)attributes.Defend; var playerDefend = (int)attributes.Defend;
var playerCriticalRate = attributes.CriticalRate; var playerCriticalRate = attributes.CriticalRate;
var monsterAttack = monster.Attack; var monsterAttack = (int)monsterStats.Attack;
var monsterDefend = monster.Defense; var monsterDefend = (int)monsterStats.Defend;
var round = 0; var round = 0;
var maxRounds = 100; var maxRounds = 100;
@ -73,7 +77,7 @@ namespace Build_God_Api.Services
if (monsterHealth <= 0) break; if (monsterHealth <= 0) break;
var monsterDamage = CalculateDamage(monsterAttack, playerDefend, monster.CriticalRate); var monsterDamage = CalculateDamage(monsterAttack, playerDefend, monsterStats.CriticalRate);
playerHealth -= monsterDamage; playerHealth -= monsterDamage;
} }

78
Build_God_Api/Build_God_Api/Services/Game/CharacterAttributeCalculateService.cs

@ -1,6 +1,5 @@
using Build_God_Api.DB; using Build_God_Api.DB;
using SqlSugar; using SqlSugar;
using System.Text.Json;
namespace Build_God_Api.Services.Game namespace Build_God_Api.Services.Game
{ {
@ -33,28 +32,10 @@ namespace Build_God_Api.Services.Game
public decimal BonusCriticalDamage { get; set; } public decimal BonusCriticalDamage { get; set; }
} }
public class CharacterAttributeCalculateService(ISqlSugarClient context): ICharacterAttributeCalculateService public class CharacterAttributeCalculateService(ISqlSugarClient context) : ICharacterAttributeCalculateService
{ {
private readonly ISqlSugarClient _context = context; private readonly ISqlSugarClient _context = context;
private decimal CalculateMaxHP(Character character, Profession? profession)
{
decimal healthRate = profession?.HealthRate ?? 1m;
return 5 * character.LevelId * character.LevelId * (decimal)Math.Log((double)(character.CurrentExp + 1000)) * healthRate;
}
private decimal CalculateAttack(Character character, Profession? profession)
{
decimal attackRate = profession?.AttackRate ?? 1m;
return 1 * character.LevelId * character.LevelId * (decimal)Math.Log((double)(character.CurrentExp + 1000)) * attackRate;
}
private decimal CalculateDefend(Character character, Profession? profession)
{
decimal defendRate = profession?.DefendRate ?? 1m;
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) private async Task<(decimal AttackBonus, decimal DefendBonus, decimal HPBonus)> CalculateScrapBonusAsync(int characterBagId)
{ {
var bagItems = await _context.Queryable<BagItem>() var bagItems = await _context.Queryable<BagItem>()
@ -96,43 +77,12 @@ namespace Build_God_Api.Services.Game
var template = await _context.Queryable<EquipmentTemplate>().FirstAsync(x => x.Id == instance.EquipmentTemplateId); var template = await _context.Queryable<EquipmentTemplate>().FirstAsync(x => x.Id == instance.EquipmentTemplateId);
if (template == null || string.IsNullOrEmpty(instance.Attributes)) continue; if (template == null || string.IsNullOrEmpty(instance.Attributes)) continue;
try var t = EquipmentAttributeBonus.SumFromJson(instance.Attributes);
{ attackBonus += t.AttackBonus;
var attributes = JsonSerializer.Deserialize<List<EquipmentAttributeDto>>(instance.Attributes); defendBonus += t.DefendBonus;
if (attributes == null) continue; hpBonus += t.HPBonus;
criticalBonus += t.CriticalBonus;
foreach (var attr in attributes) criticalDamage += t.CriticalDamageBonus;
{
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.CriticalRate:
criticalBonus += attr.Value;
break;
case EquipmentAttributeType.CriticalDamage:
criticalDamage += attr.Value;
break;
}
}
}
catch
{
// ignore parse errors
}
} }
return (attackBonus, defendBonus, hpBonus, criticalBonus, criticalDamage); return (attackBonus, defendBonus, hpBonus, criticalBonus, criticalDamage);
@ -146,9 +96,9 @@ namespace Build_God_Api.Services.Game
profession = await _context.Queryable<Profession>().FirstAsync(x => x.Id == character.ProfessionId); profession = await _context.Queryable<Profession>().FirstAsync(x => x.Id == character.ProfessionId);
} }
decimal baseMaxHP = CalculateMaxHP(character, profession); decimal baseMaxHP = CombatAttributeFormula.CalculateBaseMaxHP(character.LevelId, character.CurrentExp, profession);
decimal baseAttack = CalculateAttack(character, profession); decimal baseAttack = CombatAttributeFormula.CalculateBaseAttack(character.LevelId, character.CurrentExp, profession);
decimal baseDefend = CalculateDefend(character, profession); decimal baseDefend = CombatAttributeFormula.CalculateBaseDefend(character.LevelId, character.CurrentExp, profession);
decimal baseCritical = 0; decimal baseCritical = 0;
decimal baseCriticalDamage = 0; decimal baseCriticalDamage = 0;
@ -162,7 +112,7 @@ namespace Build_God_Api.Services.Game
if (characterBag != null) if (characterBag != null)
{ {
var (scrapAttack, scrapDefend, scrapHP) = await CalculateScrapBonusAsync(characterBag.Id); var (scrapAttack, scrapDefend, scrapHP) = await CalculateScrapBonusAsync(characterBag.Id);
var (equipAttack, equipDefend, equipHP, equipCritical,equipmentCriticalDamage) = await CalculateEquipmentBonusAsync(characterBag.Id); var (equipAttack, equipDefend, equipHP, equipCritical, equipmentCriticalDamage) = await CalculateEquipmentBonusAsync(characterBag.Id);
bonusAttack = scrapAttack + equipAttack; bonusAttack = scrapAttack + equipAttack;
bonusDefend = scrapDefend + equipDefend; bonusDefend = scrapDefend + equipDefend;
@ -199,10 +149,4 @@ namespace Build_God_Api.Services.Game
await _context.Updateable(character).ExecuteCommandAsync(); await _context.Updateable(character).ExecuteCommandAsync();
} }
} }
public class EquipmentAttributeDto
{
public EquipmentAttributeType Type { get; set; }
public decimal Value { get; set; }
}
} }

28
Build_God_Api/Build_God_Api/Services/Game/CombatAttributeFormula.cs

@ -0,0 +1,28 @@
using Build_God_Api.DB;
namespace Build_God_Api.Services.Game
{
/// <summary>
/// 角色与怪物共用的基础属性公式(等级、经验项、职业倍率)
/// </summary>
public static class CombatAttributeFormula
{
public static decimal CalculateBaseMaxHP(int levelId, decimal expForLog, Profession? profession)
{
decimal healthRate = profession?.HealthRate ?? 1m;
return 5 * levelId * levelId * (decimal)Math.Log((double)(expForLog + 1000)) * healthRate;
}
public static decimal CalculateBaseAttack(int levelId, decimal expForLog, Profession? profession)
{
decimal attackRate = profession?.AttackRate ?? 1m;
return 1 * levelId * levelId * (decimal)Math.Log((double)(expForLog + 1000)) * attackRate;
}
public static decimal CalculateBaseDefend(int levelId, decimal expForLog, Profession? profession)
{
decimal defendRate = profession?.DefendRate ?? 1m;
return 0.5m * levelId * levelId * (decimal)Math.Log((double)(expForLog + 1000)) * defendRate;
}
}
}

88
Build_God_Api/Build_God_Api/Services/Game/EquipmentAttributeBonus.cs

@ -0,0 +1,88 @@
using Build_God_Api.DB;
using System.Text.Json;
namespace Build_God_Api.Services.Game
{
public class EquipmentAttributeDto
{
public EquipmentAttributeType Type { get; set; }
public decimal Value { get; set; }
}
/// <summary>
/// 从装备属性 JSON(与 EquipmentInstance.Attributes 同格式)累加固定类加成
/// </summary>
public static class EquipmentAttributeBonus
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public static (decimal AttackBonus, decimal DefendBonus, decimal HPBonus, decimal CriticalBonus, decimal CriticalDamageBonus) SumFromJson(string? attributesJson)
{
decimal attackBonus = 0;
decimal defendBonus = 0;
decimal hpBonus = 0;
decimal criticalBonus = 0;
decimal criticalDamage = 0;
if (string.IsNullOrWhiteSpace(attributesJson)) return (attackBonus, defendBonus, hpBonus, criticalBonus, criticalDamage);
try
{
var attributes = JsonSerializer.Deserialize<List<EquipmentAttributeDto>>(attributesJson, JsonOptions);
if (attributes == null) return (attackBonus, defendBonus, hpBonus, criticalBonus, criticalDamage);
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.CriticalRate:
criticalBonus += attr.Value;
break;
case EquipmentAttributeType.CriticalDamage:
criticalDamage += attr.Value;
break;
}
}
}
catch
{
// ignore parse errors — same as character path
}
return (attackBonus, defendBonus, hpBonus, criticalBonus, criticalDamage);
}
public static (decimal AttackBonus, decimal DefendBonus, decimal HPBonus, decimal CriticalBonus, decimal CriticalDamageBonus) SumFromManyJson(IEnumerable<string?> jsonList)
{
decimal a = 0, d = 0, h = 0, c = 0, cd = 0;
foreach (var j in jsonList)
{
var t = SumFromJson(j);
a += t.AttackBonus;
d += t.DefendBonus;
h += t.HPBonus;
c += t.CriticalBonus;
cd += t.CriticalDamageBonus;
}
return (a, d, h, c, cd);
}
}
}

65
Build_God_Api/Build_God_Api/Services/Game/MonsterAttributeCalculateService.cs

@ -0,0 +1,65 @@
using Build_God_Api.DB;
using SqlSugar;
namespace Build_God_Api.Services.Game
{
public interface IMonsterAttributeCalculateService
{
Task<MonsterCombatAttributes> CalculateAsync(Monster monster, IReadOnlyList<MonsterEquipment>? equipment = null);
}
public class MonsterCombatAttributes
{
public decimal MaxHP { get; set; }
public decimal BaseMaxHP { get; set; }
public decimal BonusMaxHP { 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 MonsterAttributeCalculateService(ISqlSugarClient db) : IMonsterAttributeCalculateService
{
private readonly ISqlSugarClient _db = db;
public async Task<MonsterCombatAttributes> CalculateAsync(Monster monster, IReadOnlyList<MonsterEquipment>? equipment = null)
{
Profession? profession = null;
if (monster.ProfessionId.HasValue && monster.ProfessionId > 0)
{
profession = await _db.Queryable<Profession>().FirstAsync(x => x.Id == monster.ProfessionId);
}
decimal baseMaxHp = CombatAttributeFormula.CalculateBaseMaxHP(monster.LevelId, monster.ConfiguredExp, profession);
decimal baseAttack = CombatAttributeFormula.CalculateBaseAttack(monster.LevelId, monster.ConfiguredExp, profession);
decimal baseDefend = CombatAttributeFormula.CalculateBaseDefend(monster.LevelId, monster.ConfiguredExp, profession);
decimal baseCrit = 0;
equipment ??= await _db.Queryable<MonsterEquipment>().Where(x => x.MonsterId == monster.Id).ToListAsync();
var bonus = EquipmentAttributeBonus.SumFromManyJson(equipment.Select(e => e.Attributes));
return new MonsterCombatAttributes
{
BaseMaxHP = baseMaxHp,
BonusMaxHP = bonus.HPBonus,
BonusAttack = bonus.AttackBonus,
BonusDefend = bonus.DefendBonus,
BonusCriticalRate = bonus.CriticalBonus,
MaxHP = baseMaxHp + bonus.HPBonus,
Attack = baseAttack + bonus.AttackBonus,
Defend = baseDefend + bonus.DefendBonus,
BaseAttack = baseAttack,
BaseDefend = baseDefend,
CriticalRate = baseCrit + bonus.CriticalBonus,
BaseCriticalRate = baseCrit,
};
}
}
}

205
Build_God_Api/Build_God_Api/Services/MonsterService.cs

@ -1,22 +1,23 @@
using Build_God_Api.DB; using Build_God_Api.DB;
using Build_God_Api.Dto; using Build_God_Api.Dto;
using Build_God_Api.Services.Game;
using SqlSugar; using SqlSugar;
namespace Build_God_Api.Services namespace Build_God_Api.Services
{ {
public interface IMonsterService public interface IMonsterService
{ {
Task<bool> Add(Monster item); Task<bool> Add(MonsterSaveDto item);
Task<bool> Delete(int id); Task<bool> Delete(int id);
Task<bool> Update(Monster item); Task<bool> Update(MonsterSaveDto item);
Task<Monster?> GetById(int id); Task<MonsterResponseDto?> GetById(int id);
Task<PagedResult<Monster>> GetAll(SearchMonsterDto dto); Task<PagedResult<MonsterResponseDto>> GetAll(SearchMonsterDto dto);
Task<bool> ExistsByNameAsync(string name); Task<bool> ExistsByNameAsync(string name, int? exceptMonsterId = null);
Task<bool> AddReward(MonsterReward reward); Task<bool> AddReward(MonsterReward reward);
@ -27,21 +28,107 @@ namespace Build_God_Api.Services
Task<List<MonsterReward>> GetRewards(int monsterId); Task<List<MonsterReward>> GetRewards(int monsterId);
} }
public class MonsterService(ISqlSugarClient db, ICurrentUserService currentUserService) : IMonsterService public class MonsterService(
ISqlSugarClient db,
ICurrentUserService currentUserService,
IMonsterAttributeCalculateService monsterAttributeCalculate) : IMonsterService
{ {
private readonly ISqlSugarClient _db = db; private readonly ISqlSugarClient _db = db;
private readonly ICurrentUserService _currentUserService = currentUserService; private readonly ICurrentUserService _currentUserService = currentUserService;
private readonly IMonsterAttributeCalculateService _monsterAttributeCalculate = monsterAttributeCalculate;
public async Task<bool> Add(Monster item) private static MonsterRewardDto MapReward(MonsterReward r) => new()
{
Id = r.Id,
MonsterId = r.MonsterId,
RewardType = (int)r.Type,
ItemId = r.ItemId,
ItemName = r.ItemName,
Count = r.Count
};
private async Task<MonsterResponseDto> ToResponseAsync(
Monster m,
IReadOnlyList<MonsterEquipment> equipmentRows,
List<MonsterRewardDto>? rewards,
Dictionary<int, string> templateNames)
{
var stats = await _monsterAttributeCalculate.CalculateAsync(m, equipmentRows);
var equipDtos = equipmentRows.Select(e => new MonsterEquipmentDto
{
Id = e.Id,
MonsterId = e.MonsterId,
EquipmentTemplateId = e.EquipmentTemplateId,
Attributes = string.IsNullOrEmpty(e.Attributes) ? "[]" : e.Attributes,
EquipmentTemplateName = templateNames.GetValueOrDefault(e.EquipmentTemplateId)
}).ToList();
return new MonsterResponseDto
{
Id = m.Id,
Name = m.Name,
Description = m.Description,
ConfiguredExp = m.ConfiguredExp,
ProfessionId = m.ProfessionId,
LevelId = m.LevelId,
Type = (int)m.Type,
Icon = m.Icon,
Health = (int)stats.MaxHP,
Attack = (int)stats.Attack,
Defense = (int)stats.Defend,
CriticalRate = stats.CriticalRate,
Rewards = rewards,
Equipment = equipDtos
};
}
private async Task<Dictionary<int, string>> LoadTemplateNamesAsync(IEnumerable<int> templateIds)
{
var ids = templateIds.Distinct().ToList();
if (ids.Count == 0) return new Dictionary<int, string>();
var templates = await _db.Queryable<EquipmentTemplate>().Where(t => ids.Contains(t.Id)).ToListAsync();
return templates.ToDictionary(t => t.Id, t => t.Name);
}
public async Task<bool> Add(MonsterSaveDto item)
{ {
var exists = await ExistsByNameAsync(item.Name); var exists = await ExistsByNameAsync(item.Name);
if (exists) if (exists)
{ {
throw new Exception($"已存在名为 {item.Name} 的怪兽"); throw new Exception($"已存在名为 {item.Name} 的怪兽");
} }
item.CreatedOn = DateTime.UtcNow;
item.CreatedBy = _currentUserService.UserId; var monster = new Monster
await _db.Insertable(item).ExecuteCommandAsync(); {
Name = item.Name,
Description = item.Description,
ConfiguredExp = item.ConfiguredExp,
ProfessionId = item.ProfessionId,
LevelId = item.LevelId,
Type = (MonsterType)item.Type,
Icon = item.Icon,
CreatedOn = DateTime.UtcNow,
CreatedBy = _currentUserService.UserId,
UpdatedOn = DateTime.UtcNow,
UpdatedBy = _currentUserService.UserId
};
var monsterId = await _db.Insertable(monster).ExecuteReturnIdentityAsync();
foreach (var eq in item.Equipment)
{
await _db.Insertable(new MonsterEquipment
{
MonsterId = monsterId,
EquipmentTemplateId = eq.EquipmentTemplateId,
Attributes = string.IsNullOrWhiteSpace(eq.Attributes) ? "[]" : eq.Attributes,
CreatedOn = DateTime.UtcNow,
CreatedBy = _currentUserService.UserId,
UpdatedOn = DateTime.UtcNow,
UpdatedBy = _currentUserService.UserId
}).ExecuteCommandAsync();
}
return true; return true;
} }
@ -50,49 +137,117 @@ namespace Build_God_Api.Services
var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == id) var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == id)
?? throw new Exception("没找到对应的怪兽"); ?? throw new Exception("没找到对应的怪兽");
await _db.Deleteable<MonsterEquipment>().Where(x => x.MonsterId == id).ExecuteCommandAsync();
await _db.Deleteable<MonsterReward>().Where(x => x.MonsterId == id).ExecuteCommandAsync(); await _db.Deleteable<MonsterReward>().Where(x => x.MonsterId == id).ExecuteCommandAsync();
await _db.Deleteable(monster).ExecuteCommandAsync(); await _db.Deleteable(monster).ExecuteCommandAsync();
return true; return true;
} }
public async Task<bool> ExistsByNameAsync(string name) public async Task<bool> ExistsByNameAsync(string name, int? exceptMonsterId = null)
{ {
return await _db.Queryable<Monster>().AnyAsync(x => x.Name == name); return await _db.Queryable<Monster>()
.Where(x => x.Name == name)
.WhereIF(exceptMonsterId != null, x => x.Id != exceptMonsterId!.Value)
.AnyAsync();
} }
public async Task<PagedResult<Monster>> GetAll(SearchMonsterDto dto) public async Task<PagedResult<MonsterResponseDto>> GetAll(SearchMonsterDto dto)
{ {
var characterLevelId = await _db.Queryable<Character>().Where(c => c.Id == dto.CharacterId).Select(c => c.LevelId).FirstAsync(); int? characterLevelId = null;
if (dto.CharacterId is int cid && cid > 0)
{
characterLevelId = await _db.Queryable<Character>()
.Where(c => c.Id == cid)
.Select(c => c.LevelId)
.FirstAsync();
}
var query = _db.Queryable<Monster>() var query = _db.Queryable<Monster>()
.WhereIF(dto.MonsterType != null, x => (int)x.Type == dto.MonsterType) .WhereIF(dto.MonsterType != null, x => (int)x.Type == dto.MonsterType)
.WhereIF(dto.LevelId != null, x => x.LevelId == dto.LevelId) .WhereIF(dto.LevelId != null, x => x.LevelId == dto.LevelId)
.WhereIF(dto.CharacterId != null, x => x.LevelId == characterLevelId) .WhereIF(characterLevelId != null, x => x.LevelId == characterLevelId)
.OrderBy(x => x.CreatedOn, OrderByType.Desc); .OrderBy(x => x.CreatedOn, OrderByType.Desc);
var list = await query.Skip((dto.PageNumber - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync(); var list = await query.Clone().Skip((dto.PageNumber - 1) * dto.PageSize).Take(dto.PageSize).ToListAsync();
var total = await query.CountAsync(); var total = await query.Clone().CountAsync();
var monsterIds = list.Select(m => m.Id).ToList();
var allEquipment = monsterIds.Count == 0
? new List<MonsterEquipment>()
: await _db.Queryable<MonsterEquipment>().Where(e => monsterIds.Contains(e.MonsterId)).ToListAsync();
var allRewards = monsterIds.Count == 0
? new List<MonsterReward>()
: await _db.Queryable<MonsterReward>().Where(r => monsterIds.Contains(r.MonsterId)).ToListAsync();
var templateNames = await LoadTemplateNamesAsync(allEquipment.Select(e => e.EquipmentTemplateId));
var rewardGroups = allRewards.GroupBy(r => r.MonsterId).ToDictionary(g => g.Key, g => g.Select(MapReward).ToList());
var equipGroups = allEquipment.GroupBy(e => e.MonsterId).ToDictionary(g => g.Key, g => g.ToList());
return new PagedResult<Monster> var items = new List<MonsterResponseDto>();
foreach (var m in list)
{
equipGroups.TryGetValue(m.Id, out var eqList);
eqList ??= [];
rewardGroups.TryGetValue(m.Id, out var rewList);
items.Add(await ToResponseAsync(m, eqList, rewList, templateNames));
}
return new PagedResult<MonsterResponseDto>
{ {
PageNumber = dto.PageNumber, PageNumber = dto.PageNumber,
TotalCount = total, TotalCount = total,
Items = list Items = items
}; };
} }
public async Task<Monster?> GetById(int id) public async Task<MonsterResponseDto?> GetById(int id)
{ {
return await _db.Queryable<Monster>().FirstAsync(x => x.Id == id); var m = await _db.Queryable<Monster>().FirstAsync(x => x.Id == id);
if (m == null) return null;
var equipment = await _db.Queryable<MonsterEquipment>().Where(e => e.MonsterId == id).ToListAsync();
var rewards = await _db.Queryable<MonsterReward>().Where(r => r.MonsterId == id).ToListAsync();
var templateNames = await LoadTemplateNamesAsync(equipment.Select(e => e.EquipmentTemplateId));
return await ToResponseAsync(m, equipment, rewards.Select(MapReward).ToList(), templateNames);
} }
public async Task<bool> Update(Monster item) public async Task<bool> Update(MonsterSaveDto item)
{ {
var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == item.Id) var monster = await _db.Queryable<Monster>().FirstAsync(x => x.Id == item.Id)
?? throw new Exception("没找到对应的怪兽"); ?? throw new Exception("没找到对应的怪兽");
item.UpdatedOn = DateTime.UtcNow;
item.UpdatedBy = _currentUserService.UserId; if (!string.Equals(monster.Name, item.Name, StringComparison.Ordinal) &&
await _db.Updateable(item).ExecuteCommandAsync(); await ExistsByNameAsync(item.Name, item.Id))
{
throw new Exception($"已存在名为 {item.Name} 的怪兽");
}
monster.Name = item.Name;
monster.Description = item.Description;
monster.ConfiguredExp = item.ConfiguredExp;
monster.ProfessionId = item.ProfessionId;
monster.LevelId = item.LevelId;
monster.Type = (MonsterType)item.Type;
monster.Icon = item.Icon;
monster.UpdatedOn = DateTime.UtcNow;
monster.UpdatedBy = _currentUserService.UserId;
await _db.Updateable(monster).ExecuteCommandAsync();
await _db.Deleteable<MonsterEquipment>().Where(x => x.MonsterId == item.Id).ExecuteCommandAsync();
foreach (var eq in item.Equipment)
{
await _db.Insertable(new MonsterEquipment
{
MonsterId = item.Id,
EquipmentTemplateId = eq.EquipmentTemplateId,
Attributes = string.IsNullOrWhiteSpace(eq.Attributes) ? "[]" : eq.Attributes,
CreatedOn = DateTime.UtcNow,
CreatedBy = _currentUserService.UserId,
UpdatedOn = DateTime.UtcNow,
UpdatedBy = _currentUserService.UserId
}).ExecuteCommandAsync();
}
return true; return true;
} }

18
Build_God_Game/src/api/monster.ts

@ -1,17 +1,29 @@
import http from './index' import http from './index'
export interface MonsterEquipmentClientDto {
id: number
monsterId: number
equipmentTemplateId: number
attributes: string
equipmentTemplateName?: string
}
export interface MonsterDto { export interface MonsterDto {
id: number id: number
name: string name: string
description: string description: string
configuredExp?: number
professionId?: number | null
levelId: number
type: number
icon?: string
/** 服务端计算后的属性 */
health: number health: number
attack: number attack: number
defense: number defense: number
criticalRate: number criticalRate: number
levelId: number
type: number
icon?: string
rewards?: MonsterRewardDto[] rewards?: MonsterRewardDto[]
equipment?: MonsterEquipmentClientDto[]
} }
export interface MonsterRewardDto { export interface MonsterRewardDto {

Loading…
Cancel
Save