Files
bl/modules/blazing/model/pet.go

471 lines
13 KiB
Go
Raw Normal View History

package model
import (
"blazing/common/data/xmlres"
2025-11-23 23:38:03 +00:00
"blazing/common/utils"
"blazing/cool"
"errors"
"fmt"
"math"
"math/rand"
"time"
"github.com/gogf/gf/v2/util/gconv"
"github.com/samber/lo"
)
const TableNamePet = "pet"
// Pet mapped from table <pet>
type Pet struct {
*cool.Model
PlayerID uint32 `gorm:"not null;index:idx_pet_by_player_id;comment:'所属玩家ID'" json:"player_id"`
Free int `gorm:"not null;default:0;comment:'是否放生'" json:"free"` //"0为放入仓库1为放入背包
CatchTime uint32 `gorm:"not null;unique;comment:'捕捉时间'" json:"catch_time"` //唯一键
2025-08-31 06:53:42 +00:00
// Owner uint32 `struc:"skip"` //仅作为存储
// FreedTime uint32 `struc:"skip"` //放生时间
//是否可交易这里应该定义在精灵ID里
//是否上架
Data string `gorm:"type:jsonb;not null;comment:'精灵全部数据'" json:"data"`
}
type PetEX struct {
Pet
Data PetInfo `orm:"data" json:"data"`
}
// PetInfo 精灵信息结构(合并后的优化版本)
type PetInfo struct {
2025-08-31 06:53:42 +00:00
// 精灵编号(@UInt long → uint32
ID uint32 `fieldDesc:"精灵编号" `
// 名字默认为全0补齐到16字节固定长度 → [16]byte
Name string `struc:"[16]byte" `
// 个体值(@UInt long → uint32
Dv uint32 `fieldDesc:"个体值" `
// 性格(@UInt long → uint32
Nature uint32 `fieldDesc:"性格" `
// 等级(@UInt long → uint32
Level uint32 `fieldDesc:"等级" `
// 当前等级已获得经验(@UInt long → uint32
Exp uint32 `fieldDesc:"当前等级已经获得的经验 2538" `
// 当前等级所需经验(@UInt long → uint32
LvExp uint32 `fieldDesc:"当前等级所需的经验" `
// 升到下一级的经验(@UInt long → uint32
NextLvExp uint32 `fieldDesc:"升到下一级的经验" `
// 当前生命(@UInt long → uint32
Hp uint32 `fieldDesc:"当前生命" `
// 最大生命(@UInt long → uint32
MaxHp uint32 `fieldDesc:"最大生命" `
// * battle_lv: atk(0), def(1), sp_atk(2), sp_def(3), spd(4), accuracy(5)
Prop [5]uint32 `fieldDesc:"属性" `
// * ev:生命学习力,攻击学习力,防御学习力,特攻学习力,特防学习力,速度学习力
Ev [6]uint32 `fieldDesc:"属性" `
SkillListLen uint32 `struc:"sizeof=SkillList"`
// 技能信息固定4条空则赋值0固定长度List → [4]SkillInfo零值即符合“赋值0”
SkillList []SkillInfo
// 捕捉时间(@UInt long → 若为时间戳用uint32若需时间类型可改为time.Time需配合序列化处理
CatchTime uint32 //`json:"-"` // 显式忽略,不参与序列化
// 捕捉地图(@UInt long → uint32
CatchMap uint32 `fieldDesc:"捕捉地图" `
// 未知默认0@UInt long → uint32
CatchRect uint32 `fieldDesc:"未知默认为0" `
// 捕获等级默认0@UInt long → uint32
CatchLevel uint32 `fieldDesc:"捕获等级 默认为0" `
EffectInfoLen uint16 `struc:"sizeof=EffectInfo"`
// 特性列表长度用UShort存储变长List → []PetEffectInfo + 长度前缀规则)
EffectInfo []PetEffectInfo `fieldDesc:"特性列表, 长度在头部以UShort存储" serialize:"lengthFirst,lengthType=uint16,type=structArray"`
// 皮肤ID默认0@UInt long → uint32
SkinID uint32 `fieldDesc:"皮肤id默认为0" `
// 是否闪光(@UInt long → uint320=否1=是)
Shiny uint32 `fieldDesc:"是不是闪" `
// AbilityType uint32 `struc:"skip"` //特性
}
// 定义常量,提升可维护性(避免魔法数字)
const (
maxSingleEV uint32 = 255 // 单个EV最大值
maxTotalEV uint32 = 510 // 6个EV总和最大值
evFieldCount = 6 // EV字段数量固定6个
)
// AddEV 优化后的EV值增加方法符合Go命名规范大写导出动词开头
// 功能为宠物6个EV值增加增量保证单个≤255、总和≤510
// 参数evadd - 6个EV字段的增量数组长度必须为6
// 返回error - 参数非法/逻辑异常时返回错误bool - 是否触发了超额削减(方便业务监控)
func (pet *PetInfo) AddEV(evadd []uint32) (bool, error) {
// 1. 参数安全校验避免数组越界panic
if len(evadd) != evFieldCount {
return false, fmt.Errorf("evadd长度必须为%d当前为%d", evFieldCount, len(evadd))
}
if len(pet.Ev) != evFieldCount {
return false, errors.New("pet.Ev未初始化或长度不为6")
}
2025-11-23 09:59:34 +00:00
// 2. 第一步:直接添加并限制单项最大值(按索引顺序处理)
var tempEV [evFieldCount]uint32
for i := 0; i < evFieldCount; i++ {
2025-11-23 09:59:34 +00:00
// 直接累加增量
tempEV[i] = pet.Ev[i] + evadd[i]
2025-11-23 09:59:34 +00:00
// 单项不超过255
if tempEV[i] > maxSingleEV {
tempEV[i] = maxSingleEV
}
}
2025-11-23 09:59:34 +00:00
// 3. 计算增量后的总和检查是否超过510
totalTemp := lo.Sum(tempEV[:])
2025-11-23 09:59:34 +00:00
// 4. 若总和超额按索引顺序0→5削减优先削减前面的字段
hasCut := false
if totalTemp > maxTotalEV {
overTotal := totalTemp - maxTotalEV // 需要削减的总量
hasCut = true
2025-11-23 09:59:34 +00:00
// 按索引顺序遍历削减从第0个字段开始依次处理
for i := 0; i < evFieldCount && overTotal > 0; i++ {
// 可削减的最大值最多削减到原始值不触碰添加前的基础EV
cutAble := tempEV[i] - pet.Ev[i]
if cutAble <= 0 {
continue // 该字段无增量可削减,跳过
}
2025-11-23 09:59:34 +00:00
// 实际削减量:取"可削减量"和"剩余需削减量"的较小值
cut := cutAble
if cut > overTotal {
cut = overTotal
}
// 执行削减
2025-11-23 09:59:34 +00:00
tempEV[i] -= cut
overTotal -= cut
}
2025-11-23 09:59:34 +00:00
// 极端情况即使削减所有增量后仍超额如原始EV总和已超510继续按顺序削减原始值
if overTotal > 0 {
for i := 0; i < evFieldCount && overTotal > 0; i++ {
// 此时可削减到0根据业务需求调整也可返回错误
cutAble := tempEV[i]
if cutAble <= 0 {
continue
}
cut := cutAble
if cut > overTotal {
cut = overTotal
}
tempEV[i] -= cut
overTotal -= cut
}
}
}
2025-11-23 09:59:34 +00:00
// 5. 将处理后的结果赋值给原EV数组
copy(pet.Ev[:], tempEV[:])
return hasCut, nil
}
func (pet *PetInfo) Cure() {
pet.Hp = pet.MaxHp
for i := 0; i < len(pet.SkillList); i++ {
maxPP, ok := xmlres.SkillMap[int(pet.SkillList[i].ID)]
// 恢复至最大PP值从配置表获取
if pet.SkillList[i].ID != 0 && ok {
pet.SkillList[i].PP = uint32(maxPP.MaxPP)
}
}
}
func (pet *PetInfo) Downgrade(level uint32) {
for pet.Level > uint32(level) {
basic, ok := xmlres.PetMAP[int(pet.ID)]
if ok {
if basic.EvolvesFrom != 0 {
pet.ID = uint32(basic.EvolvesFrom)
}
}
pet.Level--
//进行降级操作
}
}
// 执行进化逻辑
func (petinfo *PetInfo) Update() {
// 最大进化次数限制(防止配置表闭环导致死循环)
maxEvolveTimes := 10
evolveCount := 0
// 循环进化:直到不满足进化条件 或 达到最大进化次数
for {
// 防止死循环,超出次数直接退出
if evolveCount >= maxEvolveTimes {
break
}
// 获取当前宠物形态的配置
basic, ok := xmlres.PetMAP[int(petinfo.ID)]
// 配置不存在,直接退出循环
if !ok {
break
}
// 检查是否满足进化条件
canEvolve := basic.EvolvesTo != 0 && // 有明确的进化目标
int(petinfo.Level) >= basic.EvolvingLv && // 等级达到进化要求
basic.IsLarge == 0 // 非最终形态
// 不满足进化条件,退出循环
if !canEvolve {
break
}
// 执行进化更新宠物ID为进化后形态
petinfo.ID = uint32(basic.EvolvesTo)
evolveCount++ // 进化次数+1
}
}
// 传入bool则不升级
// Update 改造为循环进化:直到宠物无法再进化为止,再更新经验
// t ...bool原参数逻辑len(t)==0时触发进化检查否则仅更新经验
func (petinfo *PetInfo) Update_EXP() {
// 进化完成后,统一更新经验(原逻辑保留)
petinfo.LvExp = petinfo.NextLvExp
// 获取最终形态的宠物配置,计算下一等级经验
basic := xmlres.PetMAP[int(petinfo.ID)]
petinfo.NextLvExp = calculateExperience(petinfo.Level, basic.GetBasic())
}
// calculateExperience 计算指定等级和种族值所需的经验值
// level: 当前等级
// baseValue: 种族值
func calculateExperience(level uint32, baseValue uint32) uint32 {
// 计算 A 部分:向上取整(3.75 * a * (a + 1))
partA := math.Ceil(3.75 * float64(level) * float64(level+1))
// 计算 B 部分:向上取整(b * log(1 + a / 100))
// 这里使用自然对数 math.Log如果想换底数可以用换底公式
partB := math.Log(1.0 + float64(level)/100.0)
partB = float64(baseValue) * partB
partB = math.Ceil(partB)
// 总经验是两部分之和,并向上取整
totalExp := math.Ceil(partA + partB)
return uint32(totalExp)
}
// PetEffectInfo 精灵特性信息结构
// <!-- NewSeIdx: 精灵特效索引 (默认0: 无效) -->
// <!-- Type: 0 - 仅单人战斗; 1 - 仅组队战斗; 2 - both; (默认0: 仅单人) -->
// <!-- Eid: 精灵特效eid (默认0: 无效) -->
// <!-- Stat: 精灵特效Stat: 0: 无效(默认值), 1: 永久, 2: 有`有效次数'的特效 3: 爆发特效 4: 异能精灵特质-->
// <!-- Times: 精灵特效可使用次数: 当type==2时有效 (默认值:0) -->
// <!-- Args: 特效参数, 不超过8个 (注意: 每个参数不能超过 65535) -->
// <!-- AdditionType:特效加成类型 1 种族值加成 2 技能威力加成 -->
type PetEffectInfo struct {
ItemID uint32 `struc:"uint32" json:"item_id"` //如果是能量珠,就显示
Idx uint16 `struc:"skip" json:"new_se_idx"`
Status byte `struc:"byte" json:"status"` //特性为1,能量珠为2
LeftCount byte `struc:"byte" json:"left_count"` //剩余次数
EID uint16 `struc:"uint16" json:"effect_id"` //特效ID
ArgsLen uint32 `struc:"sizeof=Args"`
Args []int ` json:"Args"` //自定义参数装载
}
// SkillInfo 精灵技能信息结构SkillInfo
type SkillInfo struct {
ID uint32
PP uint32
}
// TableName Pet's table name
func (*Pet) TableName() string {
return TableNamePet
}
// GroupName Pet's table group
func (*Pet) GroupName() string {
return "default"
}
// NewPet create a new Pet
func NewPet() *Pet {
return &Pet{
Model: cool.NewModel(),
}
}
// init 创建表
func init() {
_ = cool.CreateTable(&Pet{})
// fmt.Println(err)
}
// GenPetInfo 生成一个新的精灵实例
// - 参数为 -1 时表示随机生成对应属性
// * @param petTypeId 精灵类型ID
// * @param individualValue 个体值0-31
// * @param natureId 性格ID0-24
// * @param abilityTypeEnum 特性类型ID0=无, >0=指定, -1=随机)
// * @param shinyid 闪光ID-1=随机)
// * @param level 等级1-100
// * @return 生成的精灵实体
func GenPetInfo(
id int,
dv, natureId, abilityTypeEnum, shinyid, level int,
) *PetInfo {
// 创建随机源
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
// 初始化精灵
p := &PetInfo{
ID: uint32(id),
CatchTime: uint32(time.Now().Unix()),
Level: uint32(level),
EffectInfo: make([]PetEffectInfo, 0),
}
// ---- 处理闪光 ----
if shinyid != -1 {
p.Shiny = uint32(shinyid)
}
// ---- 性格 ----
if natureId == -1 {
p.Nature = uint32(rng.Intn(25))
} else {
p.Nature = uint32(natureId)
}
// ---- 个体值DV----
if dv == -1 {
p.Dv = uint32(CalculateIndividualValue(rng))
} else {
if dv < 0 {
dv = 0
} else if dv > 31 {
dv = 31
}
p.Dv = uint32(dv)
}
// ---- 特性 ----
switch {
case abilityTypeEnum == 0:
// 无特性
case abilityTypeEnum > 0:
// 指定特性
if v, ok := xmlres.PlayerEffectMAP[int(abilityTypeEnum)]; ok {
p.EffectInfo = append(p.EffectInfo, PetEffectInfo{
Idx: uint16(gconv.Int16(v.Idx)),
Status: 1,
EID: uint16(gconv.Int16(v.Eid)),
Args: v.ArgsS,
})
}
case abilityTypeEnum == -1:
// 随机特性
randomIndex := rng.Intn(len(xmlres.PlayerEffectMAP))
var i int
for _, v := range xmlres.PlayerEffectMAP {
if i == randomIndex {
p.EffectInfo = append(p.EffectInfo, PetEffectInfo{
Idx: uint16(gconv.Int16(v.Idx)),
Status: 1,
EID: uint16(gconv.Int16(v.Eid)),
Args: v.ArgsS,
})
break
}
i++
}
}
// ---- 技能学习 ----
2025-11-23 23:38:03 +00:00
skills := utils.LastFourElements(p.GetLevelRangeCanLearningSkills(0, p.Level), 4) // 最后四个技能
for i := 0; i < len(skills) && i < 4; i++ {
skillID := skills[i]
if info, ok := xmlres.SkillMap[int(skillID)]; ok {
p.SkillList = append(p.SkillList, SkillInfo{ID: skillID, PP: uint32(info.MaxPP)})
}
}
if len(p.SkillList) > 4 {
p.SkillList = p.SkillList[:4]
}
// ---- 属性计算 ----
p.CalculatePetPane()
p.Update_EXP()
return p
}
// 除数数组放大100倍
// 数组按递增顺序排列,用于判断个体值等级
var divisors = []int{
600, 1200, 1900, 2700, 3600, 4600, 5700, 6900, 8200, 9600,
11100, 12700, 14400, 16200, 18100, 20100, 22100, 24000,
25800, 27500, 29100, 30600, 32000, 33300, 34500, 35600,
36600, 37500, 38300, 39000, 39600,
}
// CalculateIndividual 根据给定的a值计算个体值
// 返回值表示a大于等于多少个除数范围0-31
func CalculateIndividual(a int) int {
individual := 0
for _, divisor := range divisors {
if a >= divisor {
individual++
} else {
break // 数组是递增的,可提前跳出循环
}
}
return individual
}
// CalculateIndividualValue 计算个体值0-31
// 接收外部随机数生成器,便于控制随机性和复用
func CalculateIndividualValue(random *rand.Rand) int {
// 生成0-40000的随机数作为个体值计算的输入
a := random.Intn(40001)
return CalculateIndividual(a)
}