feat(fight_boss): 优化BOSS战斗奖励逻辑并修复宠物等级突破100级限制

重构了handleMapBossFightRewards函数,将奖励逻辑分离到独立的处理函数中,
增加了shouldGrantBossWinBonus条件判断,确保只有满足条件时才发放胜利奖励。

同时修复了宠物等级系统,允许宠物等级突破100级限制但面板属性仍保持100级上限,
改进了经验获取和面板更新逻辑。

fix(item
This commit is contained in:
昔念
2026-04-14 00:38:50 +08:00
parent 62d93f65e7
commit b953e7831a
9 changed files with 110 additions and 161 deletions

View File

@@ -41,7 +41,6 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
if err != 0 { if err != 0 {
return nil, err return nil, err
} }
leadMonster := &monsterInfo.PetList[0]
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode) p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode)
@@ -53,7 +52,12 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
var fightC *fight.FightC var fightC *fight.FightC
fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) { fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) {
handleMapBossFightRewards(p, fightC, foi, mapNode, bossConfigs[0], leadMonster) if mapNode.WinBonusID == 0 {
return
}
if shouldGrantBossWinBonus(fightC, p.Info.UserID, bossConfigs[0], foi) {
p.SptCompletedTask(mapNode.WinBonusID, 1)
}
}) })
if err != 0 { if err != 0 {
return nil, err return nil, err
@@ -230,95 +234,6 @@ func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig c
return true return true
} }
func handleMapBossFightRewards(
p *player.Player,
fightC *fight.FightC,
foi model.FightOverInfo,
mapNode *configmodel.MapNode,
bossConfig configmodel.BossConfig,
leadMonster *model.PetInfo,
) {
rewards := grantMonsterFightRewards(p, foi, leadMonster)
if mapNode != nil && mapNode.WinBonusID != 0 && shouldGrantBossWinBonus(fightC, p.Info.UserID, bossConfig, foi) {
appendBossTaskReward(p, mapNode.WinBonusID, 1, rewards)
}
if rewards != nil && rewards.HasReward() {
p.SendPackCmd(8004, rewards)
}
}
func grantMonsterFightRewards(p *player.Player, foi model.FightOverInfo, monster *model.PetInfo) *fightinfo.S2C_GET_BOSS_MONSTER {
rewards := &fightinfo.S2C_GET_BOSS_MONSTER{}
if p == nil || monster == nil || foi.Reason != 0 || foi.WinnerId != p.Info.UserID || !p.CanGet() {
return rewards
}
petCfg, ok := xmlres.PetMAP[int(monster.ID)]
if !ok {
return rewards
}
exp := uint32(petCfg.YieldingExp) * monster.Level / 7
addlevel, poolevel := p.CanGetExp()
addexp := gconv.Float32(addlevel * gconv.Float32(exp))
poolexp := gconv.Float32(poolevel) * gconv.Float32(exp)
p.ItemAdd(3, int64(poolexp+addexp))
rewards.AddItem(rewardItemExpPool, uint32(poolexp))
p.AddPetExp(foi.Winpet, int64(addexp))
if p.CanGetItem() {
itemID := p.GetSpace().GetDrop()
if itemID != 0 {
count := uint32(grand.N(1, 2))
if p.ItemAdd(itemID, int64(count)) {
rewards.AddItem(uint32(itemID), count)
}
}
}
petType := int64(petCfg.Type)
if monster.IsShiny() && p.CanGetXUAN() && petType < 16 {
xuanID := uint32(400686 + petType)
count := uint32(grand.N(1, 2))
if p.ItemAdd(int64(xuanID), int64(count)) {
rewards.AddItem(xuanID, count)
}
}
if foi.Winpet != nil {
foi.Winpet.AddEV(petCfg.YieldingEVValues)
}
return rewards
}
func appendBossTaskReward(p *player.Player, taskID int, outState int, rewards *fightinfo.S2C_GET_BOSS_MONSTER) {
if p == nil || rewards == nil || !p.IsLogin || taskID <= 0 {
return
}
if p.Info.GetTask(taskID) == model.Completed {
return
}
granted, err := p.ApplyTaskCompletion(uint32(taskID), outState, nil)
if err != 0 {
return
}
p.Info.SetTask(taskID, model.Completed)
rewards.BonusID = uint32(taskID)
if granted == nil {
return
}
if granted.Pet != nil {
rewards.PetID = granted.Pet.ID
rewards.CaptureTm = granted.Pet.CatchTime
}
for _, item := range granted.Items {
rewards.AddItemInfo(item)
}
}
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) { func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
if refPet.ID == 0 { if refPet.ID == 0 {
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
@@ -354,8 +269,46 @@ func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInf
} }
func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *model.PetInfo) { func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *model.PetInfo) {
rewards := grantMonsterFightRewards(p, foi, monster) if foi.Reason != 0 || foi.WinnerId != p.Info.UserID || !p.CanGet() {
return
}
petCfg, ok := xmlres.PetMAP[int(monster.ID)]
if !ok {
return
}
exp := uint32(petCfg.YieldingExp) * monster.Level / 7
addlevel, poolevel := p.CanGetExp()
addexp := gconv.Float32(addlevel * gconv.Float32(exp))
poolexp := gconv.Float32(poolevel) * gconv.Float32(exp)
rewards := &fightinfo.S2C_GET_BOSS_MONSTER{}
p.ItemAdd(3, int64(poolexp+addexp))
rewards.AddItem(rewardItemExpPool, uint32(poolexp))
p.AddPetExp(foi.Winpet, int64(addexp))
if p.CanGetItem() {
itemID := p.GetSpace().GetDrop()
if itemID != 0 {
count := uint32(grand.N(1, 2))
if p.ItemAdd(itemID, int64(count)) {
rewards.AddItem(uint32(itemID), count)
}
}
}
petType := int64(petCfg.Type)
if monster.IsShiny() && p.CanGetXUAN() && petType < 16 {
xuanID := uint32(400686 + petType)
count := uint32(grand.N(1, 2))
if p.ItemAdd(int64(xuanID), int64(count)) {
rewards.AddItem(xuanID, count)
}
}
if rewards.HasReward() { if rewards.HasReward() {
p.SendPackCmd(8004, rewards) p.SendPackCmd(8004, rewards)
} }
foi.Winpet.AddEV(petCfg.YieldingEVValues)
} }

View File

@@ -15,6 +15,8 @@ import (
const ( const (
// ItemDefaultLeftTime 道具默认剩余时间(毫秒) // ItemDefaultLeftTime 道具默认剩余时间(毫秒)
ItemDefaultLeftTime = 360000 ItemDefaultLeftTime = 360000
// UniversalNatureItemID 全能性格转化剂Ω
UniversalNatureItemID uint32 = 300136
) )
// GetUserItemList 获取用户道具列表 // GetUserItemList 获取用户道具列表
@@ -187,6 +189,14 @@ func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (r
return nil, errorcode.ErrorCodes.Err10401 return nil, errorcode.ErrorCodes.Err10401
} }
if data.ItemId != UniversalNatureItemID {
return nil, errorcode.ErrorCodes.ErrItemUnusable
}
if _, ok := xmlres.NatureRootMap[int(data.Nature)]; !ok {
return nil, errorcode.ErrorCodes.ErrItemUnusable
}
if c.Service.Item.CheakItem(data.ItemId) <= 0 { if c.Service.Item.CheakItem(data.ItemId) <= 0 {
return nil, errorcode.ErrorCodes.ErrInsufficientItems return nil, errorcode.ErrorCodes.ErrInsufficientItems
} }

View File

@@ -175,7 +175,7 @@ func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
// } // }
f.overl.Do(func() { f.overl.Do(func() {
f.Reason = res f.Reason = normalizeFightOverReason(res)
if f.GetInputByPlayer(c, true) != nil { if f.GetInputByPlayer(c, true) != nil {
f.WinnerId = f.GetInputByPlayer(c, true).UserID f.WinnerId = f.GetInputByPlayer(c, true).UserID
} }

View File

@@ -7,12 +7,21 @@ import "blazing/modules/player/model"
// 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape. // 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape.
func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo { func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo {
payload := over payload := over
payload.Reason = mapFightOverReasonFor2506(over.Reason) payload.Reason = model.EnumBattleOverReason(mapUnifiedFightOverReason(over.Reason))
return &payload return &payload
} }
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason { func normalizeFightOverReason(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
switch reason { if reason == model.BattleOverReason.DefaultEnd {
return 0
}
return reason
}
func mapUnifiedFightOverReason(reason model.EnumBattleOverReason) uint32 {
switch normalizeFightOverReason(reason) {
case 0, model.BattleOverReason.Cacthok:
return 0
case model.BattleOverReason.PlayerOffline: case model.BattleOverReason.PlayerOffline:
return 1 return 1
case model.BattleOverReason.PlayerOVerTime: case model.BattleOverReason.PlayerOVerTime:
@@ -20,12 +29,12 @@ func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBatt
case model.BattleOverReason.NOTwind: case model.BattleOverReason.NOTwind:
return 3 return 3
case model.BattleOverReason.PlayerEscape: case model.BattleOverReason.PlayerEscape:
// Player-initiated escape is handled by 2410 on the flash side; 2506 should return 5
// still land in a non-error bucket instead of "system error".
return 1
case model.BattleOverReason.Cacthok, model.BattleOverReason.DefaultEnd:
return 0
default: default:
return 4 return 4
} }
} }
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
return model.EnumBattleOverReason(mapUnifiedFightOverReason(reason))
}

View File

@@ -522,9 +522,9 @@ func (f *FightC) TURNOVER(cur *input.Input) {
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束 if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID
f.FightOverInfo.Reason = model.BattleOverReason.DefaultEnd f.FightOverInfo.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.WinnerId = f.FightOverInfo.WinnerId f.WinnerId = f.FightOverInfo.WinnerId
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = f.FightOverInfo.Reason
f.closefight = true f.closefight = true
// break // break

View File

@@ -426,38 +426,15 @@ func (f *FightC) buildLegacyGroupOverInfo(over *model.FightOverInfo) *legacyGrou
} }
func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 { func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 {
switch reason { return mapUnifiedFightOverReason(reason)
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.NOTwind:
return 4
case model.BattleOverReason.DefaultEnd:
return 1
case model.BattleOverReason.PlayerEscape:
return 6
default:
return 5
}
} }
func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 { func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 {
if over == nil { if over == nil {
return 5 return mapUnifiedFightOverReason(0)
}
switch over.Reason {
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.PlayerEscape:
return 6
case model.BattleOverReason.NOTwind:
return 4
} }
if over.WinnerId != 0 { if over.WinnerId != 0 {
return 1 return mapUnifiedFightOverReason(0)
} }
return mapLegacyGroupFightOverReason(over.Reason) return mapLegacyGroupFightOverReason(over.Reason)
} }

View File

@@ -76,7 +76,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOppPlayer(); player != nil { if player := f.primaryOppPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID f.WinnerId = player.GetInfo().UserID
} }
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason f.FightOverInfo.Reason = f.Reason
f.closefight = true f.closefight = true
@@ -86,7 +86,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOurPlayer(); player != nil { if player := f.primaryOurPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID f.WinnerId = player.GetInfo().UserID
} }
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason f.FightOverInfo.Reason = f.Reason
f.closefight = true f.closefight = true

View File

@@ -27,33 +27,24 @@ func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
if petInfo == nil || addExp <= 0 { if petInfo == nil || addExp <= 0 {
return return
} }
if petInfo.Level >= 100 { if petInfo.Level > 100 {
petInfo.Level = 100 currentHP := petInfo.Hp
petInfo.Exp = 0
petInfo.Update(false) petInfo.Update(false)
petInfo.CalculatePetPane(100) petInfo.CalculatePetPane(100)
if petInfo.Hp > petInfo.MaxHp { petInfo.Hp = utils.Min(currentHP, petInfo.MaxHp)
petInfo.Hp = petInfo.MaxHp
}
return
} }
addExp = utils.Min(addExp, p.Info.ExpPool) addExp = utils.Min(addExp, p.Info.ExpPool)
originalLevel := petInfo.Level originalLevel := petInfo.Level
exp := int64(petInfo.Exp) + addExp exp := int64(petInfo.Exp) + addExp
p.Info.ExpPool -= addExp //减去已使用的经验 p.Info.ExpPool -= addExp //减去已使用的经验
gainedExp := exp //已获得的经验 gainedExp := exp //已获得的经验
for petInfo.Level < 100 && exp >= int64(petInfo.NextLvExp) { for exp >= int64(petInfo.NextLvExp) {
petInfo.Level++ petInfo.Level++
exp -= int64(petInfo.LvExp) exp -= int64(petInfo.LvExp)
petInfo.Update(true) petInfo.Update(true)
} }
if petInfo.Level >= 100 {
p.Info.ExpPool += exp // 超出100级上限的经验退回经验池
gainedExp -= exp
exp = 0
}
petInfo.Exp = (exp) petInfo.Exp = (exp)
// 重新计算面板 // 重新计算面板
if originalLevel != petInfo.Level { if originalLevel != petInfo.Level {

View File

@@ -17,12 +17,16 @@ func firstPetIDForTest(t *testing.T) int {
return 0 return 0
} }
func TestAddPetExpStopsAtLevel100(t *testing.T) { func TestAddPetExpAllowsLevelBeyond100WhilePanelStaysCapped(t *testing.T) {
petID := firstPetIDForTest(t) petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 99, nil, 0) petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
if petInfo == nil { if petInfo == nil {
t.Fatalf("failed to generate test pet") t.Fatalf("failed to generate test pet")
} }
if expectedPanel == nil {
t.Fatalf("failed to generate expected test pet")
}
player := &Player{ player := &Player{
baseplayer: baseplayer{ baseplayer: baseplayer{
@@ -34,21 +38,29 @@ func TestAddPetExpStopsAtLevel100(t *testing.T) {
player.AddPetExp(petInfo, petInfo.NextLvExp+10_000) player.AddPetExp(petInfo, petInfo.NextLvExp+10_000)
if petInfo.Level != 100 { if petInfo.Level <= 100 {
t.Fatalf("expected pet level to stop at 100, got %d", petInfo.Level) t.Fatalf("expected pet level to continue beyond 100, got %d", petInfo.Level)
} }
if petInfo.Exp != 0 { if petInfo.MaxHp != expectedPanel.MaxHp {
t.Fatalf("expected pet exp to reset at level cap, got %d", petInfo.Exp) t.Fatalf("expected max hp to stay capped at 100-level panel, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
}
if petInfo.Prop != expectedPanel.Prop {
t.Fatalf("expected props to stay capped at 100-level panel, got %+v want %+v", petInfo.Prop, expectedPanel.Prop)
} }
} }
func TestAddPetExpDoesNotConsumePoolAboveLevel100(t *testing.T) { func TestAddPetExpRecalculatesPanelForLevelAbove100(t *testing.T) {
petID := firstPetIDForTest(t) petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0) petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
if petInfo == nil { if petInfo == nil {
t.Fatalf("failed to generate test pet") t.Fatalf("failed to generate test pet")
} }
if expectedPanel == nil {
t.Fatalf("failed to generate expected test pet")
}
petInfo.Level = 101 petInfo.Level = 101
petInfo.Exp = 7
petInfo.MaxHp = 1 petInfo.MaxHp = 1
petInfo.Hp = 999999 petInfo.Hp = 999999
@@ -62,19 +74,16 @@ func TestAddPetExpDoesNotConsumePoolAboveLevel100(t *testing.T) {
player.AddPetExp(petInfo, 12_345) player.AddPetExp(petInfo, 12_345)
if petInfo.Level != 100 { if petInfo.Level < 101 {
t.Fatalf("expected level to be normalized to 100, got %d", petInfo.Level) t.Fatalf("expected level above 100 to be preserved, got %d", petInfo.Level)
} }
if player.Info.ExpPool != 50_000 { if petInfo.MaxHp != expectedPanel.MaxHp {
t.Fatalf("expected exp pool to remain unchanged, got %d", player.Info.ExpPool) t.Fatalf("expected max hp to be recalculated using level 100 cap, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
}
if petInfo.Exp != 0 {
t.Fatalf("expected exp to reset after normalization, got %d", petInfo.Exp)
}
if petInfo.MaxHp <= 1 {
t.Fatalf("expected pet panel to be recalculated, got max hp %d", petInfo.MaxHp)
} }
if petInfo.Hp != petInfo.MaxHp { if petInfo.Hp != petInfo.MaxHp {
t.Fatalf("expected hp to be clamped to recalculated max hp, got hp=%d maxHp=%d", petInfo.Hp, petInfo.MaxHp) t.Fatalf("expected hp to be clamped to recalculated max hp, got hp=%d maxHp=%d", petInfo.Hp, petInfo.MaxHp)
} }
if player.Info.ExpPool != 50_000-12_345 {
t.Fatalf("expected exp pool to be consumed normally, got %d", player.Info.ExpPool)
}
} }