Compare commits

...

8 Commits

Author SHA1 Message Date
昔念
de6c700bb3 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 01:30:48 +08:00
昔念
3232efd05a 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-17 00:48:43 +08:00
昔念
0c79fee8af 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-17 00:35:17 +08:00
昔念
3d77e146e9 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-16 23:49:28 +08:00
xinian
a43a25c610 test: cover legacy round broadcast handling
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-16 10:25:56 +08:00
xinian
3cfde577eb test: add pet fusion transaction coverage
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-16 09:21:39 +08:00
xinian
85f9c02ced fix: correct self-destruct mutual KO handling
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-16 09:21:02 +08:00
昔念
9f7fd83626 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 22:42:56 +08:00
12 changed files with 271 additions and 50 deletions

File diff suppressed because one or more lines are too long

View File

@@ -19,20 +19,13 @@ func (h Controller) SavePetBagOrder(
return nil, 0
}
// PetRetrieveFromWarehouse 领回仓库精灵
// PetRetrieveFromWarehouse 从放生仓库领回精灵
func (h Controller) PetRetrieveFromWarehouse(
data *PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if _, ok := player.FindPetBagSlot(data.CatchTime); ok {
return nil, 0
if !player.Service.Pet.UpdateFree(data.CatchTime, 1, 0) {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
petInfo := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
if petInfo == nil {
return nil, 0
}
player.AddPetToAvailableBag(petInfo.Data)
return nil, 0
}

View File

@@ -48,9 +48,8 @@ func (h Controller) PetReleaseToWarehouse(
if inBag || inBackup || freeForbidden == 1 {
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
}
if !player.Service.Pet.UpdateFree(data.CatchTime, 1) {
return nil, errorcode.ErrorCodes.ErrSystemError
if !player.Service.Pet.UpdateFree(data.CatchTime, 0, 1) {
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
}
return nil, 0

View File

@@ -140,7 +140,7 @@ func (e *Effect1181) OnSkill() bool {
type Effect1182 struct{ node.EffectNode }
func (e *Effect1182) Skill_Use() bool {
if len(e.Args()) < 2 || e.Ctx().Our == nil || e.Ctx().Our.CurPet[0] == nil || e.Ctx().Opp == nil || e.Ctx().Opp.CurPet[0] == nil {
if len(e.Args()) < 2 || e.Ctx().Our == nil || e.Ctx().Our.CurPet[0] == nil {
return true
}
@@ -153,9 +153,15 @@ func (e *Effect1182) Skill_Use() bool {
if targetHP.Cmp(alpacadecimal.Zero) < 0 {
targetHP = alpacadecimal.Zero
}
if e.Ctx().Opp.CurPet[0].GetHP().Cmp(targetHP) > 0 {
e.Ctx().Opp.CurPet[0].Info.Hp = uint32(targetHP.IntPart())
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
if target.CurrentPet().GetHP().Cmp(targetHP) > 0 {
target.CurrentPet().Info.Hp = uint32(targetHP.IntPart())
}
return true
})
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1182, int(e.Args()[1].IntPart()))
if sub != nil {

View File

@@ -207,9 +207,15 @@ func registerSelfDamageOnSkillEffects() {
})
}
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: opponentDamage,
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: opponentDamage,
})
return true
})
return true
},
@@ -241,9 +247,15 @@ func registerSelfDamageSkillUseEffects() {
Type: info.DamageType.Fixed,
Damage: damage,
})
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
})
return true
})
return true
},
@@ -253,9 +265,23 @@ func registerSelfDamageSkillUseEffects() {
Damage: alpacadecimal.NewFromInt(int64(e.Ctx().Our.CurPet[0].Info.MaxHp)),
})
damage := int64(grand.N(250, 300))
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), e.Ctx().Opp.CurPet[0].GetHP().Sub(alpacadecimal.NewFromInt(1))),
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil {
return true
}
targetPet := target.CurrentPet()
if targetPet == nil {
return true
}
remainHP := targetPet.GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), remainHP),
})
return true
})
return true
},
@@ -280,15 +306,25 @@ func registerSelfDamageSkillUseEffects() {
randomDamage = grand.N(minDamage, maxDamage)
}
remainHP := e.Ctx().Opp.CurPet[0].GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil {
return true
}
targetPet := target.CurrentPet()
if targetPet == nil {
return true
}
remainHP := targetPet.GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP)
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP)
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
})
return true
})
return true
},
@@ -297,11 +333,17 @@ func registerSelfDamageSkillUseEffects() {
return true
}
applyAllPropDown(e.Ctx().Our, e.Ctx().Opp, int8(e.Args()[0].IntPart()))
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1380, int(e.Args()[1].IntPart()), int(e.Args()[2].IntPart()))
if sub != nil {
e.Ctx().Opp.AddEffect(e.Ctx().Our, sub)
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
applyAllPropDown(e.Ctx().Our, target, int8(e.Args()[0].IntPart()))
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1380, int(e.Args()[1].IntPart()), int(e.Args()[2].IntPart()))
if sub != nil {
target.AddEffect(e.Ctx().Our, sub)
}
return true
})
e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: e.Ctx().Our.CurPet[0].GetHP(),

View File

@@ -7,6 +7,8 @@ import (
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
_ "blazing/logic/service/fight/itemover"
_ "blazing/logic/service/fight/rule"
"blazing/modules/player/model"
"reflect"
@@ -133,7 +135,20 @@ func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*i
if skillAction == nil {
return nil, nil
}
return f.GetInputByAction(skillAction, false), f.GetInputByAction(skillAction, true)
attacker := f.GetInputByAction(skillAction, false)
defender := f.GetInputByAction(skillAction, true)
if attacker != nil && defender == attacker && shouldResolveOpponentAsTarget(skillAction.SkillEntity) {
if opponent, _ := attacker.OpponentSlotAtOrNextLiving(0); opponent != nil {
defender = opponent
} else if opponent := f.roundOpponentInput(attacker); opponent != nil {
defender = opponent
}
}
return attacker, defender
}
func shouldResolveOpponentAsTarget(skill *info.SkillEntity) bool {
return skill != nil && skill.XML.AtkType == 3
}
// setEffectSkillContext 统一设置技能阶段 effect 上下文。
@@ -232,6 +247,15 @@ func (f *FightC) roundOpponentInput(attacker *input.Input) *input.Input {
return nil
}
func shouldSkipSecondAction(first, second *input.Input) bool {
if first == nil || second == nil {
return false
}
firstPet := first.CurrentPet()
secondPet := second.CurrentPet()
return firstPet == nil || firstPet.Info.Hp <= 0 || secondPet == nil || secondPet.Info.Hp <= 0
}
// enterturn 处理战斗回合逻辑
// 回合有先手方和后手方,同时有攻击方和被攻击方
func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) {
@@ -333,6 +357,10 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
if currentAction == nil {
continue
}
if shouldSkipSecondAction(f.First, f.Second) {
secondAttack = nil
continue
}
attacker, defender = f.getSkillParticipants(secondAttack)
originalSkill = f.copySkill(secondAttack)
//取消后手历史效果

View File

@@ -7,6 +7,7 @@ import (
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
// <!--
// GBTL:
// 1. AtkNum:本技能同时攻击数量, 默认:1(不能为0)
@@ -496,15 +497,23 @@ func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.Sele
if f == nil || !f.LegacyGroupProtocol {
return
}
if firstAttack != nil {
if f.legacySkillExecuted(firstAttack) {
f.sendLegacyGroupSkillHurt(firstAttack)
}
if secondAttack != nil {
if f.legacySkillExecuted(secondAttack) {
f.sendLegacyGroupSkillHurt(secondAttack)
}
f.sendLegacyGroupBoutDone()
}
func (f *FightC) legacySkillExecuted(skillAction *action.SelectSkillAction) bool {
if f == nil || skillAction == nil {
return false
}
attacker := f.GetInputByAction(skillAction, false)
return attacker != nil && attacker.AttackValue != nil && attacker.AttackValue.SkillID != 0
}
func (f *FightC) sendLegacyGroupSkillHurt(skillAction *action.SelectSkillAction) {
if f == nil || !f.LegacyGroupProtocol || skillAction == nil {
return
@@ -584,10 +593,10 @@ func (f *FightC) buildLegacyGroupSkillAttackInfo(skillAction *action.SelectSkill
if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID)
}
if skillAction != nil && skillAction.SkillEntity != nil {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
} else {
if attackValue.SkillID != 0 {
result.MoveID = attackValue.SkillID
} else if skillAction != nil && skillAction.SkillEntity != nil {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
}
result.IsCrit = attackValue.IsCritical
result.EffectName = attackValue.State

View File

@@ -0,0 +1,61 @@
package fight
import (
"testing"
"blazing/logic/service/fight/action"
fightinfo "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
func TestSendLegacyRoundBroadcastSkipsUnexecutedAction(t *testing.T) {
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
our := input.NewInput(nil, ourPlayer)
our.InitAttackValue()
our.AttackValue.SkillID = 111
our.SetCurPetAt(0, fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 11,
Hp: 80,
MaxHp: 100,
CatchTime: 101,
}))
opp := input.NewInput(nil, oppPlayer)
opp.InitAttackValue()
opp.SetCurPetAt(0, fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 22,
Hp: 0,
MaxHp: 100,
CatchTime: 202,
}))
fc := &FightC{
Our: []*input.Input{our},
Opp: []*input.Input{opp},
LegacyGroupProtocol: true,
}
firstAttack := &action.SelectSkillAction{
BaseAction: action.BaseAction{PlayerID: ourPlayer.info.UserID, ActorIndex: 0, TargetIndex: 0},
}
secondAttack := &action.SelectSkillAction{
BaseAction: action.BaseAction{PlayerID: oppPlayer.info.UserID, ActorIndex: 0, TargetIndex: 0},
}
fc.sendLegacyRoundBroadcast(firstAttack, secondAttack)
for _, player := range []*stubPlayer{ourPlayer, oppPlayer} {
if len(player.sentCmds) != 2 {
t.Fatalf("expected one skill packet plus bout done, got %v", player.sentCmds)
}
if player.sentCmds[0] != groupCmdSkillHurt {
t.Fatalf("expected first packet to be skill hurt, got %d", player.sentCmds[0])
}
if player.sentCmds[1] != groupCmdBoutDone {
t.Fatalf("expected second packet to be bout done, got %d", player.sentCmds[1])
}
}
}

View File

@@ -12,7 +12,8 @@ import (
)
type stubPlayer struct {
info model.PlayerInfo
info model.PlayerInfo
sentCmds []uint32
}
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
@@ -26,7 +27,7 @@ func (*stubPlayer) SetFightC(common.FightI) {}
func (*stubPlayer) QuitFight() {}
func (*stubPlayer) MessWin(bool) {}
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 }
func (*stubPlayer) SendPackCmd(uint32, any) {}
func (p *stubPlayer) SendPackCmd(cmd uint32, _ any) { p.sentCmds = append(p.sentCmds, cmd) }
func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {

View File

@@ -76,8 +76,12 @@ func (s *PetService) PetCount(flag int) int {
return ret
}
func (s *PetService) UpdateFree(catchTime, free uint32) bool {
res, err := s.dbm(s.Model).Where("catch_time", catchTime).Data("free", free).Update()
func (s *PetService) UpdateFree(catchTime, fromFree, toFree uint32) bool {
res, err := s.dbm(s.Model).
Where("catch_time", catchTime).
Where("free", fromFree).
Data("free", toFree).
Update()
if err != nil {
return false
}

View File

@@ -72,6 +72,9 @@ func (s *UserService) PetFusionTx(
return nil
}
if err := syncPetSnapshotBeforeDeleteTx(tx, userID, currentInfo, masterCatchTime); err != nil {
return err
}
if err := deletePetTx(tx, userID, masterCatchTime); err != nil {
return err
}
@@ -82,6 +85,9 @@ func (s *UserService) PetFusionTx(
}
result.CostItemUsed = used
if !used {
if err := syncPetSnapshotBeforeDeleteTx(tx, userID, currentInfo, auxCatchTime); err != nil {
return err
}
if err := deletePetTx(tx, userID, auxCatchTime); err != nil {
return err
}
@@ -223,6 +229,28 @@ func updatePetDataTx(tx gdb.TX, userID uint32, catchTime uint32, pet model.PetIn
return nil
}
func syncPetSnapshotBeforeDeleteTx(tx gdb.TX, userID uint32, info model.PlayerInfo, catchTime uint32) error {
pet, ok := findPetDataSnapshotInPlayerInfo(info, catchTime)
if !ok {
return nil
}
return updatePetDataTx(tx, userID, catchTime, pet)
}
func findPetDataSnapshotInPlayerInfo(info model.PlayerInfo, catchTime uint32) (model.PetInfo, bool) {
for i := range info.PetList {
if info.PetList[i].CatchTime == catchTime {
return info.PetList[i], true
}
}
for i := range info.BackupPetList {
if info.BackupPetList[i].CatchTime == catchTime {
return info.BackupPetList[i], true
}
}
return model.PetInfo{}, false
}
func deletePetTx(tx gdb.TX, userID uint32, catchTime uint32) error {
res, err := tx.Model(model.NewPet()).
Where("player_id", userID).

View File

@@ -0,0 +1,50 @@
package service
import (
"blazing/modules/player/model"
"testing"
)
func TestFindPetDataSnapshotInPlayerInfo(t *testing.T) {
t.Run("pet list", func(t *testing.T) {
want := model.PetInfo{CatchTime: 1001, Level: 55}
info := model.PlayerInfo{
PetList: []model.PetInfo{want},
}
got, ok := findPetDataSnapshotInPlayerInfo(info, want.CatchTime)
if !ok {
t.Fatal("expected pet snapshot in pet list")
}
if got.CatchTime != want.CatchTime || got.Level != want.Level {
t.Fatalf("unexpected pet snapshot: %+v", got)
}
})
t.Run("backup pet list", func(t *testing.T) {
want := model.PetInfo{CatchTime: 2002, Level: 66}
info := model.PlayerInfo{
BackupPetList: []model.PetInfo{want},
}
got, ok := findPetDataSnapshotInPlayerInfo(info, want.CatchTime)
if !ok {
t.Fatal("expected pet snapshot in backup pet list")
}
if got.CatchTime != want.CatchTime || got.Level != want.Level {
t.Fatalf("unexpected pet snapshot: %+v", got)
}
})
t.Run("not found", func(t *testing.T) {
info := model.PlayerInfo{
PetList: []model.PetInfo{{CatchTime: 3003}},
BackupPetList: []model.PetInfo{{CatchTime: 4004}},
}
_, ok := findPetDataSnapshotInPlayerInfo(info, 9999)
if ok {
t.Fatal("expected missing pet snapshot")
}
})
}