diff --git a/logic/controller/item_use.go b/logic/controller/item_use.go index 3d403fb53..f54d511b1 100644 --- a/logic/controller/item_use.go +++ b/logic/controller/item_use.go @@ -166,14 +166,7 @@ func refreshPetPaneKeepHP(currentPet *model.PetInfo, hp uint32) { // handleRegularPetItem 处理普通宠物道具 func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInfo) errorcode.ErrorCode { - handler := item.PetItemRegistry.GetHandler(itemID) - if handler == nil { - return errorcode.ErrorCodes.ErrItemUnusable - } - if !handler(itemID, currentPet) { - return errorcode.ErrorCodes.ErrItemUnusable - } - return 0 + return item.PetItemRegistry.Handle(itemID, currentPet) } // ResetNature 重置宠物性格 diff --git a/logic/service/fight/boss/NewSeIdx_247.go b/logic/service/fight/boss/NewSeIdx_247.go new file mode 100644 index 000000000..1a87d4d7b --- /dev/null +++ b/logic/service/fight/boss/NewSeIdx_247.go @@ -0,0 +1,75 @@ +package effect + +import ( + "blazing/logic/service/fight/action" + "blazing/logic/service/fight/input" +) + +// 247. 固定增加体力/攻击/防御/特攻/特防/速度;(a1-a6: hp/atk/def/spatk/spdef/spd) +type NewSel247 struct { + NewSel0 +} + +func (e *NewSel247) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { + if !e.IsOwner() { + return + } + + pet := e.Ctx().Our.CurPet[0] + if pet == nil { + return + } + + hpBonus := uint32(e.Args()[0].IntPart()) + if hpBonus > 0 { + pet.Info.MaxHp += hpBonus + pet.Info.Hp += hpBonus + } + + for i, propIdx := range []int{0, 1, 2, 3, 4} { + add := uint32(e.Args()[i+1].IntPart()) + if add == 0 { + continue + } + pet.Info.Prop[propIdx] += add + } +} + +func (e *NewSel247) TurnEnd() { + if !e.IsOwner() { + return + } + + pet := e.Ctx().Our.CurPet[0] + if pet == nil { + return + } + + hpBonus := uint32(e.Args()[0].IntPart()) + if hpBonus > 0 { + if pet.Info.MaxHp > hpBonus { + pet.Info.MaxHp -= hpBonus + } else { + pet.Info.MaxHp = 1 + } + if pet.Info.Hp > pet.Info.MaxHp { + pet.Info.Hp = pet.Info.MaxHp + } + } + + for i, propIdx := range []int{0, 1, 2, 3, 4} { + sub := uint32(e.Args()[i+1].IntPart()) + if sub == 0 { + continue + } + if pet.Info.Prop[propIdx] > sub { + pet.Info.Prop[propIdx] -= sub + } else { + pet.Info.Prop[propIdx] = 1 + } + } +} + +func init() { + input.InitEffect(input.EffectType.NewSel, 247, &NewSel247{}) +} diff --git a/logic/service/fight/boss/NewSeIdx_26.go b/logic/service/fight/boss/NewSeIdx_26.go index d22172f5b..83bbf1e9e 100644 --- a/logic/service/fight/boss/NewSeIdx_26.go +++ b/logic/service/fight/boss/NewSeIdx_26.go @@ -11,9 +11,15 @@ type NewSel26 struct { } func (e *NewSel26) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { + if !e.IsOwner() { + return + } e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] += uint32(e.Args()[1].IntPart()) } func (e *NewSel26) TurnEnd() { + if !e.IsOwner() { + return + } e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] -= uint32(e.Args()[1].IntPart()) } func init() { diff --git a/logic/service/fight/cmd_unified.go b/logic/service/fight/cmd_unified.go index 55a646d98..6352b4aa6 100644 --- a/logic/service/fight/cmd_unified.go +++ b/logic/service/fight/cmd_unified.go @@ -27,16 +27,26 @@ const ( // 2. targetIndex 始终表示目标在所属阵营内的槽位。 // 3. targetRelation 用来区分 targetIndex 属于敌方、自己还是队友。 type FightActionEnvelope struct { - ActionType FightActionType `json:"actionType"` - ActorIndex int `json:"actorIndex"` - TargetIndex int `json:"targetIndex"` - TargetRelation uint8 `json:"targetRelation,omitempty"` - SkillID uint32 `json:"skillId,omitempty"` - ItemID uint32 `json:"itemId,omitempty"` - CatchTime uint32 `json:"catchTime,omitempty"` - Escape bool `json:"escape,omitempty"` - Chat string `json:"chat,omitempty"` - AtkType uint8 `json:"atkType,omitempty"` + // ActionType 当前动作类型,例如 skill、item、change、escape、chat。 + ActionType FightActionType `json:"actionType"` + // ActorIndex 发起动作的我方槽位。 + ActorIndex int `json:"actorIndex"` + // TargetIndex 目标在所属阵营中的槽位下标。 + TargetIndex int `json:"targetIndex"` + // TargetRelation 目标关系:0=对方,1=自己,2=队友。 + TargetRelation uint8 `json:"targetRelation,omitempty"` + // SkillID 技能 ID;仅技能动作使用。 + SkillID uint32 `json:"skillId,omitempty"` + // ItemID 道具 ID;仅道具动作使用。 + ItemID uint32 `json:"itemId,omitempty"` + // CatchTime 精灵实例 ID;切宠或部分道具动作使用。 + CatchTime uint32 `json:"catchTime,omitempty"` + // Escape 是否为逃跑动作;主要用于协议层兼容和调试。 + Escape bool `json:"escape,omitempty"` + // Chat 聊天内容;仅聊天动作使用。 + Chat string `json:"chat,omitempty"` + // AtkType 前端技能目标类型兜底值,沿用技能表 AtkType 定义。 + AtkType uint8 `json:"atkType,omitempty"` } // NewSkillActionEnvelope 构造技能动作 envelope。 diff --git a/logic/service/fight/info/unified_info.go b/logic/service/fight/info/unified_info.go index 21d5981e7..5c07d7695 100644 --- a/logic/service/fight/info/unified_info.go +++ b/logic/service/fight/info/unified_info.go @@ -45,7 +45,7 @@ type FighterState struct { Level uint32 `json:"level"` // Anger 怒气值;当前服务端主链路暂未实际填充时默认为 0,先为协议对齐预留。 Anger uint32 `json:"anger"` - // Status 当前异常/增益状态回合数组;下标语义沿用现有战斗状态定义。 + // Status 当前异常或增益状态回合数组;下标语义沿用现有战斗状态定义。 Status [20]int8 `json:"status"` // Prop 当前能力等级变化数组:攻击、防御、特攻、特防、速度、命中。 Prop [6]int8 `json:"prop"` @@ -55,30 +55,42 @@ type FighterState struct { // FightStateMeta 是统一状态包的公共元数据。 type FightStateMeta struct { - Round uint32 `json:"round"` - Weather uint32 `json:"weather,omitempty"` - WinnerID uint32 `json:"winnerId,omitempty"` - Reason model.EnumBattleOverReason `json:"reason,omitempty"` - LegacyCmd uint32 `json:"legacyCmd,omitempty"` + // Round 当前回合数。 + Round uint32 `json:"round"` + // Weather 当前天气或场地编号;当前主链路未填充时可为 0。 + Weather uint32 `json:"weather,omitempty"` + // WinnerID 当前已确定的胜者 ID;未结束时通常为 0。 + WinnerID uint32 `json:"winnerId,omitempty"` + // Reason 当前已确定的结束原因;未结束时通常为 0。 + Reason model.EnumBattleOverReason `json:"reason,omitempty"` + // LegacyCmd 对应旧协议命令号,便于新旧包对照和过渡期调试。 + LegacyCmd uint32 `json:"legacyCmd,omitempty"` } -// FightSkillHurtState 保存技能结算后的左右两侧战报快照。 +// FightSkillHurtState 保存技能结算阶段的详细战报。 type FightSkillHurtState struct { - Left []model.AttackValue `json:"left,omitempty"` + // Left 我方阵营本次技能结算后的攻击值快照列表。 + Left []model.AttackValue `json:"left,omitempty"` + // Right 敌方阵营本次技能结算后的攻击值快照列表。 Right []model.AttackValue `json:"right,omitempty"` } // FightLoadState 保存加载进度信息。 type FightLoadState struct { - UserID uint32 `json:"userId"` + // UserID 当前上报加载进度的玩家 ID。 + UserID uint32 `json:"userId"` + // Percent 当前加载百分比。 Percent uint32 `json:"percent"` } // FightChatState 保存战斗内聊天信息。 type FightChatState struct { - SenderID uint32 `json:"senderId"` + // SenderID 发言玩家 ID。 + SenderID uint32 `json:"senderId"` + // SenderNickname 发言玩家昵称。 SenderNickname string `json:"senderNickname"` - Message string `json:"message"` + // Message 聊天内容。 + Message string `json:"message"` } // FightStateEnvelope 是统一出站状态结构。 diff --git a/logic/service/fight/loop.go b/logic/service/fight/loop.go index 24e0a9953..c7c47e4b9 100644 --- a/logic/service/fight/loop.go +++ b/logic/service/fight/loop.go @@ -24,6 +24,26 @@ import ( "github.com/jinzhu/copier" ) +func consumeLimitedPetEffects(pet *model.PetInfo) { + if pet == nil || len(pet.EffectInfo) == 0 { + return + } + + next := pet.EffectInfo[:0] + for _, eff := range pet.EffectInfo { + if eff.Status == 2 { + if eff.LeftCount > 0 { + eff.LeftCount-- + } + if eff.LeftCount == 0 { + continue + } + } + next = append(next, eff) + } + pet.EffectInfo = next +} + func (f *FightC) battleLoop() { defer func() { if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值 @@ -69,26 +89,28 @@ func (f *FightC) battleLoop() { tt.Alive(false) //将所有属性变化失效掉 return true }) - if f.Info.Mode != info.BattleMode.PET_MELEE { //不是乱斗,传回血量 - for i := 0; i < len(ff.AllPet); i++ { - for j := 0; j < len(ff.Player.GetInfo().PetList); j++ { - if ff.Player.GetInfo().PetList[j].CatchTime == ff.AllPet[i].Info.CatchTime { - - if ff.UserID == f.WinnerId { - currentPet := ff.CurrentPet() - if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime { - f.Winpet = &ff.Player.GetInfo().PetList[j] - } - - } - - ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp) - ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList - } - + for i := 0; i < len(ff.AllPet); i++ { + consumeLimitedPetEffects(&ff.AllPet[i].Info) + for j := 0; j < len(ff.Player.GetInfo().PetList); j++ { + if ff.Player.GetInfo().PetList[j].CatchTime != ff.AllPet[i].Info.CatchTime { + continue } - } + ff.Player.GetInfo().PetList[j].EffectInfo = ff.AllPet[i].Info.EffectInfo + if f.Info.Mode == info.BattleMode.PET_MELEE { + continue + } + + if ff.UserID == f.WinnerId { + currentPet := ff.CurrentPet() + if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime { + f.Winpet = &ff.Player.GetInfo().PetList[j] + } + } + + ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp) + ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList + } } }) diff --git a/logic/service/item/petuse.go b/logic/service/item/petuse.go index f8362adcd..c2bff532a 100644 --- a/logic/service/item/petuse.go +++ b/logic/service/item/petuse.go @@ -2,6 +2,7 @@ package item import ( "blazing/common/data/xmlres" + "blazing/common/socket/errorcode" "blazing/common/utils" "blazing/modules/player/model" "strings" @@ -111,6 +112,62 @@ func nvfunc(itemid uint32, onpet *model.PetInfo) bool { return true } +func handleNewSeIdxPetItem(itemid uint32, onpet *model.PetInfo) errorcode.ErrorCode { + itemCfg, ok := xmlres.ItemsMAP[int(itemid)] + if !ok || itemCfg.NewSeIdx == 0 { + return errorcode.ErrorCodes.ErrItemUnusable + } + + effectCfg, ok := xmlres.EffectMAP[itemCfg.NewSeIdx] + if !ok { + return errorcode.ErrorCodes.ErrSystemError + } + + effectStatus := byte(gconv.Int(effectCfg.Stat)) + effectIdx := uint16(itemCfg.NewSeIdx) + leftCount := 1 + if effectCfg.Times != nil && *effectCfg.Times != "" { + leftCount = gconv.Int(*effectCfg.Times) + if leftCount <= 0 { + leftCount = 1 + } + } + + limitedCount := 0 + for _, eff := range onpet.EffectInfo { + if eff.Idx == effectIdx { + return errorcode.ErrorCodes.ErrCannotInjectPillAgain + } + if eff.Status == 2 { + limitedCount++ + } + } + if effectStatus == 2 && limitedCount >= 2 { + return errorcode.ErrorCodes.ErrTooManyEnergyOrbs + } + + onpet.EffectInfo = append(onpet.EffectInfo, model.PetEffectInfo{ + ItemID: itemid, + Idx: effectIdx, + Status: effectStatus, + LeftCount: byte(leftCount), + EID: uint16(gconv.Int(effectCfg.Eid)), + Args: effectCfg.ArgsS, + }) + return 0 +} + +func (r *PetItemHandlerRegistry) Handle(itemID uint32, onpet *model.PetInfo) errorcode.ErrorCode { + handler := r.GetHandler(itemID) + if handler != nil { + if handler(itemID, onpet) { + return 0 + } + return errorcode.ErrorCodes.ErrItemUnusable + } + return handleNewSeIdxPetItem(itemID, onpet) +} + // -------------------------- 6. 初始化注册器(注册所有处理器) -------------------------- func init() { diff --git a/modules/config/controller/admin/sign.go b/modules/config/controller/admin/sign.go new file mode 100644 index 000000000..03d89ed05 --- /dev/null +++ b/modules/config/controller/admin/sign.go @@ -0,0 +1,20 @@ +package admin + +import ( + "blazing/cool" + "blazing/modules/config/service" +) + +type SignController struct { + *cool.Controller +} + +func init() { + cool.RegisterController(&SignController{ + &cool.Controller{ + Prefix: "/admin/config/sign", + Api: []string{"Add", "Delete", "Update", "Info", "List", "Page"}, + Service: service.NewSignInService(), + }, + }) +} diff --git a/modules/config/model/sign.go b/modules/config/model/sign.go new file mode 100644 index 000000000..abbd0a238 --- /dev/null +++ b/modules/config/model/sign.go @@ -0,0 +1,31 @@ +package model + +import ( + "blazing/cool" +) + +const TableNameSignIn = "config_sign_in" + +// SignIn 签到活动配置表。 +type SignIn struct { + *cool.Model + SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'签到活动ID'" json:"sign_in_id"` + Status uint32 `gorm:"not null;default:0;comment:'签到状态(0-未启用 1-启用)'" json:"status"` + RewardScript string `gorm:"type:varchar(2048);default:'';comment:'签到奖励配置(JSON)'" json:"reward_script"` +} + +func (*SignIn) TableName() string { + return TableNameSignIn +} + +func (*SignIn) GroupName() string { + return "default" +} + +func NewSignIn() *SignIn { + return &SignIn{Model: cool.NewModel()} +} + +func init() { + cool.CreateTable(&SignIn{}) +} diff --git a/modules/config/service/sign.go b/modules/config/service/sign.go new file mode 100644 index 000000000..ba3ba03e8 --- /dev/null +++ b/modules/config/service/sign.go @@ -0,0 +1,37 @@ +package service + +import ( + "blazing/cool" + "blazing/modules/config/model" +) + +type SignInService struct { + *cool.Service +} + +func (s *SignInService) GetActive(signInID uint32) *model.SignIn { + m := cool.DBM(s.Model) + if signInID != 0 { + m.Where("sign_in_id", signInID) + } + m.Where("status", 1) + m.Order("sign_in_id", "asc") + + var out *model.SignIn + m.Scan(&out) + return out +} + +func NewSignInService() *SignInService { + return &SignInService{ + &cool.Service{ + Model: model.NewSignIn(), + ListQueryOp: &cool.QueryOp{ + FieldEQ: []string{"sign_in_id", "status"}, + }, + PageQueryOp: &cool.QueryOp{ + FieldEQ: []string{"sign_in_id", "status"}, + }, + }, + } +} diff --git a/modules/player/controller/admin/sign.go b/modules/player/controller/admin/sign.go new file mode 100644 index 000000000..8a69323f9 --- /dev/null +++ b/modules/player/controller/admin/sign.go @@ -0,0 +1,20 @@ +package admin + +import ( + "blazing/cool" + "blazing/modules/player/service" +) + +type SignRecordController struct { + *cool.Controller +} + +func init() { + cool.RegisterController(&SignRecordController{ + &cool.Controller{ + Prefix: "/admin/game/signrecord", + Api: []string{"Delete", "Update", "Info", "List", "Page"}, + Service: service.NewSignService(0), + }, + }) +} diff --git a/modules/player/controller/app/sign.go b/modules/player/controller/app/sign.go new file mode 100644 index 000000000..ee5f73db7 --- /dev/null +++ b/modules/player/controller/app/sign.go @@ -0,0 +1,106 @@ +package app + +import ( + "blazing/cool" + configservice "blazing/modules/config/service" + playerservice "blazing/modules/player/service" + "context" + "fmt" + "strings" + + "github.com/deatil/go-cryptobin/cryptobin/crypto" + "github.com/gogf/gf/v2/frame/g" +) + +type SignController struct { + *cool.Controller +} + +func init() { + controller := &SignController{ + &cool.Controller{ + Prefix: "/seer/game/sign", + Api: []string{}, + Service: configservice.NewSignInService(), + }, + } + cool.RegisterController(controller) +} + +type SignStateReq struct { + g.Meta `path:"/state" method:"GET"` + UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"` + Session string `json:"session" v:"required#session不能为空"` + SignInID uint32 `json:"sign_in_id" d:"1" v:"min:1#签到活动ID非法"` +} + +type SignClaimReq struct { + g.Meta `path:"/claim" method:"POST"` + UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"` + Session string `json:"session" v:"required#session不能为空"` + SignInID uint32 `json:"sign_in_id" d:"1" v:"min:1#签到活动ID非法"` +} + +func (c *SignController) State(ctx context.Context, req *SignStateReq) (res *cool.BaseRes, err error) { + if err = g.Validator().Data(req).Run(ctx); err != nil { + return cool.Fail(err.Error()), nil + } + if err = validateGameSession(req.UserID, req.Session); err != nil { + return cool.Fail(err.Error()), nil + } + + state, err := playerservice.NewSignService(req.UserID).GetState(req.SignInID) + if err != nil { + return cool.Fail(err.Error()), nil + } + return cool.Ok(state), nil +} + +func (c *SignController) Claim(ctx context.Context, req *SignClaimReq) (res *cool.BaseRes, err error) { + if err = g.Validator().Data(req).Run(ctx); err != nil { + return cool.Fail(err.Error()), nil + } + if err = validateGameSession(req.UserID, req.Session); err != nil { + return cool.Fail(err.Error()), nil + } + + result, err := playerservice.NewSignService(req.UserID).Claim(req.SignInID) + if err != nil { + return cool.Fail(err.Error()), nil + } + return cool.Ok(result), nil +} + +func validateGameSession(userID uint32, session string) error { + if userID == 0 { + return fmt.Errorf("user_id不能为空") + } + session = strings.TrimSpace(session) + if session == "" { + return fmt.Errorf("session不能为空") + } + + cached, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", userID)) + if err != nil || cached.IsEmpty() { + return fmt.Errorf("session已过期,请重新登录") + } + + rawSession := session + decrypted := crypto. + FromBase64String(session). + SetKey("gfertf12dfertf12"). + SetIv("gfertf12dfertf12"). + Aes(). + CBC(). + PKCS7Padding(). + Decrypt(). + ToString() + if decrypted != "" { + rawSession = decrypted + } + + if rawSession != cached.String() { + return fmt.Errorf("session无效,请重新登录") + } + return nil +} diff --git a/modules/player/model/pet.go b/modules/player/model/pet.go index 6b9cb4d03..5e8bec9a0 100644 --- a/modules/player/model/pet.go +++ b/modules/player/model/pet.go @@ -388,7 +388,7 @@ func (pet *PetInfo) RnadEffect() { func (pet *PetInfo) GetEffect(ptype int) (int, *PetEffectInfo, bool) { return utils.FindWithIndex(pet.EffectInfo, func(item PetEffectInfo) bool { - return item.Status == 1 + return int(item.Status) == ptype }) } diff --git a/modules/player/model/sign.go b/modules/player/model/sign.go index 20dc281cb..406d94f44 100644 --- a/modules/player/model/sign.go +++ b/modules/player/model/sign.go @@ -16,9 +16,12 @@ type SignInRecord struct { PlayerID uint32 `gorm:"not null;index:idx_player_id;comment:'玩家ID'" json:"player_id"` SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'关联的签到活动ID(对应player_sign_in表的SignInID)'" json:"sign_in_id"` - IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"` - //通过bitset来实现签到的进度记录 - SignInProgress []uint32 `gorm:"type:jsonb;not null;comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"` + IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"` + ContinuousDays uint32 `gorm:"not null;default:0;comment:'连续签到天数'" json:"continuous_days"` + TotalDays uint32 `gorm:"not null;default:0;comment:'累计签到天数'" json:"total_days"` + LastSignDate string `gorm:"type:varchar(10);not null;default:'';comment:'最近一次签到日期(YYYY-MM-DD)'" json:"last_sign_date"` + // 通过 bitset 记录每日签到状态,位索引从 0 开始,对应签到第 1 天。 + SignInProgress []uint32 `gorm:"type:jsonb;not null;default:'[]';comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"` } // TableName 指定表名(遵循现有规范) diff --git a/modules/player/model/user_sign.go b/modules/player/model/user_sign.go index afbdce2f2..4b1d59360 100644 --- a/modules/player/model/user_sign.go +++ b/modules/player/model/user_sign.go @@ -1,40 +1,14 @@ package model -import ( - "blazing/cool" -) +import configmodel "blazing/modules/config/model" -// 表名常量(遵循现有命名规范:小写+下划线) -const TableNameSignIn = "config_sign_in" +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +const TableNameSignIn = configmodel.TableNameSignIn -// SignIn 签到记录表 -// 核心字段:签到完成状态、状压签到进度、签到奖励脚本 -type SignIn struct { - *cool.Model // 嵌入基础Model(包含主键、创建/更新时间等通用字段) - SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'签到活动ID'" json:"sign_in_id"` - Status uint32 `gorm:"not null;default:0;comment:'签到状态(0-未完成 1-已完成)'" json:"status"` - //传入用户名,签到天数,给予奖励,这个搭配里程碑表实现 - RewardScript string `gorm:"type:varchar(512);default:'';comment:'签到奖励脚本(执行奖励发放的脚本内容)'" json:"reward_script"` -} - -// TableName 指定表名(遵循现有规范) -func (*SignIn) TableName() string { - return TableNameSignIn -} - -// GroupName 指定表分组(默认分组,与现有Item表/精灵特效表一致) -func (*SignIn) GroupName() string { - return "default" -} - -// NewSignIn 创建签到记录表实例(初始化基础Model) -func NewSignIn() *SignIn { - return &SignIn{ - Model: cool.NewModel(), - } -} - -// init 程序启动时自动创建表(与现有PlayerPetSpecialEffect表的初始化逻辑一致) -func init() { - cool.CreateTable(&SignIn{}) +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +type SignIn = configmodel.SignIn + +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +func NewSignIn() *configmodel.SignIn { + return configmodel.NewSignIn() } diff --git a/modules/player/service/sign.go b/modules/player/service/sign.go new file mode 100644 index 000000000..faf5f54a1 --- /dev/null +++ b/modules/player/service/sign.go @@ -0,0 +1,554 @@ +package service + +import ( + "blazing/common/data" + "blazing/cool" + baseservice "blazing/modules/base/service" + configservice "blazing/modules/config/service" + "blazing/modules/player/model" + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/pointernil/bitset32" +) + +// SignRewardItem 定义单日签到奖励中的物品条目。 +type SignRewardItem struct { + ItemID uint32 `json:"item_id"` + Count int64 `json:"count"` +} + +// SignRewardDay 定义单日签到奖励。 +type SignRewardDay struct { + Day uint32 `json:"day"` + GiftItemIDs []uint32 `json:"gift_item_ids,omitempty"` + Items []SignRewardItem `json:"items,omitempty"` + PetRewardIDs []uint32 `json:"pet_reward_ids,omitempty"` + TitleRewardIDs []uint32 `json:"title_reward_ids,omitempty"` + Coins int64 `json:"coins,omitempty"` + Gold int64 `json:"gold,omitempty"` + FreeGold int64 `json:"free_gold,omitempty"` + ExpPool int64 `json:"exp_pool,omitempty"` + EVPool int64 `json:"ev_pool,omitempty"` +} + +// SignRewardPreview 用于返回签到面板里的奖励和领取状态。 +type SignRewardPreview struct { + SignRewardDay + Signed bool `json:"signed"` +} + +// SignState 表示玩家当前活动的签到状态。 +type SignState struct { + SignInID uint32 `json:"sign_in_id"` + Status uint32 `json:"status"` + TotalDays uint32 `json:"total_days"` + ContinuousDays uint32 `json:"continuous_days"` + LastSignDate string `json:"last_sign_date"` + TodaySigned bool `json:"today_signed"` + Completed bool `json:"completed"` + CanSignToday bool `json:"can_sign_today"` + NextSignDay uint32 `json:"next_sign_day"` + SignedDays []uint32 `json:"signed_days"` + Rewards []SignRewardPreview `json:"rewards"` +} + +// SignGrantResult 表示本次签到实际发放的奖励。 +type SignGrantResult struct { + Items []data.ItemInfo `json:"items,omitempty"` + PetIDs []uint32 `json:"pet_ids,omitempty"` + TitleIDs []uint32 `json:"title_ids,omitempty"` + Coins int64 `json:"coins,omitempty"` + Gold int64 `json:"gold,omitempty"` + FreeGold int64 `json:"free_gold,omitempty"` + ExpPool int64 `json:"exp_pool,omitempty"` + EVPool int64 `json:"ev_pool,omitempty"` + RewardDay uint32 `json:"reward_day"` +} + +// SignClaimResult 表示签到后的完整结果。 +type SignClaimResult struct { + State *SignState `json:"state"` + Reward *SignGrantResult `json:"reward"` +} + +type signRewardPayload struct { + Days []SignRewardDay `json:"days"` +} + +// SignService 管理玩家签到进度。 +type SignService struct { + BaseService +} + +func NewSignService(id uint32) *SignService { + return &SignService{ + BaseService: BaseService{ + userid: id, + Service: &cool.Service{ + Model: model.NewSignInRecord(), + ListQueryOp: &cool.QueryOp{ + FieldEQ: []string{"player_id", "sign_in_id", "is_completed"}, + }, + PageQueryOp: &cool.QueryOp{ + FieldEQ: []string{"player_id", "sign_in_id", "is_completed"}, + }, + }, + }, + } +} + +func (s *SignService) GetState(signInID uint32) (*SignState, error) { + cfg := configservice.NewSignInService().GetActive(signInID) + if cfg == nil { + return nil, fmt.Errorf("签到活动不存在或未启用") + } + + rewards, err := parseRewardDays(cfg.RewardScript) + if err != nil { + return nil, err + } + if len(rewards) == 0 { + return nil, fmt.Errorf("签到活动未配置奖励") + } + + record, err := s.getRecord(cfg.SignInID) + if err != nil { + return nil, err + } + + return buildSignState(cfg.SignInID, cfg.Status, rewards, record), nil +} + +func (s *SignService) Claim(signInID uint32) (*SignClaimResult, error) { + cfg := configservice.NewSignInService().GetActive(signInID) + if cfg == nil { + return nil, fmt.Errorf("签到活动不存在或未启用") + } + + rewards, err := parseRewardDays(cfg.RewardScript) + if err != nil { + return nil, err + } + if len(rewards) == 0 { + return nil, fmt.Errorf("签到活动未配置奖励") + } + + record, isNew, err := s.getOrInitRecord(cfg.SignInID) + if err != nil { + return nil, err + } + + today := currentDateString() + if record.LastSignDate == today { + return nil, fmt.Errorf("今天已经签到过了") + } + + nextDay, reward := nextRewardDay(rewards, record) + if reward == nil || nextDay == 0 { + return nil, fmt.Errorf("当前签到活动已全部完成") + } + + prevDate := record.LastSignDate + progress := progressBitset(record.SignInProgress) + progress.Set(uint(nextDay - 1)) + record.SignInProgress = progress.Bytes() + record.TotalDays = uint32(progress.Count()) + record.LastSignDate = today + if isYesterday(prevDate, today) { + record.ContinuousDays++ + } else { + record.ContinuousDays = 1 + } + _, pendingReward := nextRewardDay(rewards, record) + record.IsCompleted = pendingReward == nil + + if err := s.saveRecord(record, isNew); err != nil { + return nil, err + } + + grant, err := s.applyReward(*reward) + if err != nil { + cool.Logger.Error(context.TODO(), "sign reward apply failed", s.userid, cfg.SignInID, nextDay, err) + return nil, err + } + grant.RewardDay = nextDay + + state := buildSignState(cfg.SignInID, cfg.Status, rewards, record) + return &SignClaimResult{State: state, Reward: grant}, nil +} + +func (s *SignService) getRecord(signInID uint32) (*model.SignInRecord, error) { + var out *model.SignInRecord + if err := s.dbm(s.Model).Where("sign_in_id", signInID).Scan(&out); err != nil { + return nil, err + } + if out != nil && out.SignInProgress == nil { + out.SignInProgress = []uint32{} + } + return out, nil +} + +func (s *SignService) getOrInitRecord(signInID uint32) (*model.SignInRecord, bool, error) { + record, err := s.getRecord(signInID) + if err != nil { + return nil, false, err + } + if record != nil { + return record, false, nil + } + + return &model.SignInRecord{ + Base: model.Base{ + Model: cool.NewModel(), + IsVip: cool.Config.ServerInfo.IsVip, + }, + PlayerID: s.userid, + SignInID: signInID, + IsCompleted: false, + ContinuousDays: 0, + TotalDays: 0, + LastSignDate: "", + SignInProgress: []uint32{}, + }, true, nil +} + +func (s *SignService) saveRecord(record *model.SignInRecord, isNew bool) error { + if record == nil { + return errors.New("签到记录为空") + } + + data := map[string]any{ + "player_id": record.PlayerID, + "sign_in_id": record.SignInID, + "is_completed": record.IsCompleted, + "continuous_days": record.ContinuousDays, + "total_days": record.TotalDays, + "last_sign_date": record.LastSignDate, + "sign_in_progress": record.SignInProgress, + "is_vip": cool.Config.ServerInfo.IsVip, + } + + if isNew { + _, err := cool.DBM(s.Model).Data(data).Insert() + return err + } + + _, err := s.dbm(s.Model).Where("sign_in_id", record.SignInID).Data(data).Update() + return err +} + +func (s *SignService) applyReward(reward SignRewardDay) (*SignGrantResult, error) { + result := &SignGrantResult{} + infoService := NewInfoService(s.userid) + playerInfo := infoService.GetLogin() + if playerInfo == nil { + return nil, fmt.Errorf("玩家角色不存在") + } + + var ( + needSaveInfo bool + bagItems []data.ItemInfo + ) + + appendRewardItem := func(itemID uint32, count int64) { + if itemID == 0 || count <= 0 { + return + } + switch itemID { + case 1: + result.Coins += count + case 3: + result.ExpPool += count + case 5: + result.Gold += count + case 9: + result.EVPool += count + default: + bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count}) + } + } + + for _, giftID := range reward.GiftItemIDs { + gift := configservice.NewItemService().GetItemCount(giftID) + appendRewardItem(uint32(gift.ItemId), gift.ItemCnt) + } + for _, item := range reward.Items { + appendRewardItem(item.ItemID, item.Count) + } + + result.Coins += reward.Coins + result.Gold += reward.Gold + result.FreeGold += reward.FreeGold + result.ExpPool += reward.ExpPool + result.EVPool += reward.EVPool + + if result.Coins != 0 { + playerInfo.Coins += result.Coins + needSaveInfo = true + } + if result.ExpPool != 0 { + playerInfo.ExpPool += result.ExpPool + needSaveInfo = true + } + if result.EVPool != 0 { + playerInfo.EVPool += result.EVPool + needSaveInfo = true + } + if needSaveInfo { + infoService.Save(*playerInfo) + } + + if result.Gold != 0 { + baseservice.NewBaseSysUserService().UpdateGold(s.userid, result.Gold*100) + } + if result.FreeGold != 0 { + baseservice.NewBaseSysUserService().UpdateFreeGold(s.userid, result.FreeGold*100) + } + + if len(bagItems) > 0 { + items, err := NewItemService(s.userid).AddItems(bagItems) + if err != nil { + return nil, err + } + result.Items = items + } + + for _, petRewardID := range reward.PetRewardIDs { + cfg := configservice.NewPetRewardService().Get(petRewardID) + if cfg == nil || cfg.MonID == 0 { + continue + } + petInfo := model.GenPetInfo(int(cfg.MonID), int(cfg.DV), int(cfg.Nature), int(cfg.Effect), int(cfg.Lv), nil, 0) + if _, err := NewPetService(s.userid).PetAdd(petInfo, 0); err != nil { + return nil, err + } + result.PetIDs = append(result.PetIDs, uint32(cfg.MonID)) + } + + for _, titleID := range reward.TitleRewardIDs { + if titleID == 0 { + continue + } + NewTitleService(s.userid).Give(titleID) + result.TitleIDs = append(result.TitleIDs, titleID) + } + + return result, nil +} + +func buildSignState(signInID, status uint32, rewards []SignRewardDay, record *model.SignInRecord) *SignState { + today := currentDateString() + progress := progressBitset(nil) + state := &SignState{ + SignInID: signInID, + Status: status, + } + + if record != nil { + progress = progressBitset(record.SignInProgress) + state.ContinuousDays = record.ContinuousDays + state.LastSignDate = record.LastSignDate + state.TodaySigned = record.LastSignDate == today + state.Completed = record.IsCompleted + } + + state.SignedDays = signedDays(progress) + state.TotalDays = uint32(len(state.SignedDays)) + if record != nil && record.TotalDays > state.TotalDays { + state.TotalDays = record.TotalDays + } + + state.Rewards = make([]SignRewardPreview, 0, len(rewards)) + for _, reward := range rewards { + state.Rewards = append(state.Rewards, SignRewardPreview{ + SignRewardDay: reward, + Signed: progress.Test(uint(reward.Day - 1)), + }) + } + + nextDay, _ := nextRewardDay(rewards, record) + state.NextSignDay = nextDay + if len(rewards) > 0 && nextDay == 0 { + state.Completed = true + } + state.CanSignToday = !state.TodaySigned && !state.Completed && nextDay != 0 + return state +} + +func nextRewardDay(rewards []SignRewardDay, record *model.SignInRecord) (uint32, *SignRewardDay) { + progress := progressBitset(nil) + if record != nil { + progress = progressBitset(record.SignInProgress) + } + for i := range rewards { + if rewards[i].Day == 0 { + continue + } + if !progress.Test(uint(rewards[i].Day - 1)) { + return rewards[i].Day, &rewards[i] + } + } + return 0, nil +} + +func parseRewardDays(raw string) ([]SignRewardDay, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + var payload signRewardPayload + if err := json.Unmarshal([]byte(raw), &payload); err == nil && len(payload.Days) > 0 { + return normalizeRewardDays(payload.Days), nil + } + + var list []SignRewardDay + if err := json.Unmarshal([]byte(raw), &list); err == nil && len(list) > 0 { + return normalizeRewardDays(list), nil + } + + var dayMap map[string]SignRewardDay + if err := json.Unmarshal([]byte(raw), &dayMap); err == nil && len(dayMap) > 0 { + list = make([]SignRewardDay, 0, len(dayMap)) + for key, reward := range dayMap { + if reward.Day == 0 { + day, convErr := strconv.ParseUint(key, 10, 32) + if convErr != nil { + return nil, fmt.Errorf("签到奖励配置中的 day 非法: %s", key) + } + reward.Day = uint32(day) + } + list = append(list, reward) + } + return normalizeRewardDays(list), nil + } + + return nil, fmt.Errorf("签到奖励配置格式不支持") +} + +func normalizeRewardDays(input []SignRewardDay) []SignRewardDay { + merged := make(map[uint32]*SignRewardDay) + for _, reward := range input { + if reward.Day == 0 { + continue + } + current, ok := merged[reward.Day] + if !ok { + copyReward := reward + copyReward.GiftItemIDs = append([]uint32{}, reward.GiftItemIDs...) + copyReward.Items = append([]SignRewardItem{}, reward.Items...) + copyReward.PetRewardIDs = append([]uint32{}, reward.PetRewardIDs...) + copyReward.TitleRewardIDs = append([]uint32{}, reward.TitleRewardIDs...) + merged[reward.Day] = ©Reward + continue + } + current.GiftItemIDs = append(current.GiftItemIDs, reward.GiftItemIDs...) + current.Items = append(current.Items, reward.Items...) + current.PetRewardIDs = append(current.PetRewardIDs, reward.PetRewardIDs...) + current.TitleRewardIDs = append(current.TitleRewardIDs, reward.TitleRewardIDs...) + current.Coins += reward.Coins + current.Gold += reward.Gold + current.FreeGold += reward.FreeGold + current.ExpPool += reward.ExpPool + current.EVPool += reward.EVPool + } + + days := make([]uint32, 0, len(merged)) + for day := range merged { + days = append(days, day) + } + sort.Slice(days, func(i, j int) bool { return days[i] < days[j] }) + + result := make([]SignRewardDay, 0, len(days)) + for _, day := range days { + reward := merged[day] + reward.GiftItemIDs = uniqueUint32(reward.GiftItemIDs) + reward.PetRewardIDs = uniqueUint32(reward.PetRewardIDs) + reward.TitleRewardIDs = uniqueUint32(reward.TitleRewardIDs) + reward.Items = normalizeRewardItems(reward.Items) + result = append(result, *reward) + } + return result +} + +func normalizeRewardItems(items []SignRewardItem) []SignRewardItem { + merged := make(map[uint32]int64) + for _, item := range items { + if item.ItemID == 0 || item.Count <= 0 { + continue + } + merged[item.ItemID] += item.Count + } + ids := make([]uint32, 0, len(merged)) + for itemID := range merged { + ids = append(ids, itemID) + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + + result := make([]SignRewardItem, 0, len(ids)) + for _, itemID := range ids { + result = append(result, SignRewardItem{ItemID: itemID, Count: merged[itemID]}) + } + return result +} + +func uniqueUint32(values []uint32) []uint32 { + seen := make(map[uint32]struct{}, len(values)) + result := make([]uint32, 0, len(values)) + for _, value := range values { + if value == 0 { + continue + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + return result +} + +func progressBitset(progress []uint32) *bitset32.BitSet32 { + if len(progress) == 0 { + return bitset32.New(0) + } + return bitset32.From(progress) +} + +func signedDays(progress *bitset32.BitSet32) []uint32 { + if progress == nil { + return []uint32{} + } + result := make([]uint32, 0, progress.Count()) + for idx, ok := progress.NextSet(0); ok; idx, ok = progress.NextSet(idx + 1) { + result = append(result, uint32(idx+1)) + } + return result +} + +func currentDateString() string { + return time.Now().Format("2006-01-02") +} + +func isYesterday(previousDate, currentDate string) bool { + if previousDate == "" || currentDate == "" { + return false + } + prev, err := time.ParseInLocation("2006-01-02", previousDate, time.Local) + if err != nil { + return false + } + curr, err := time.ParseInLocation("2006-01-02", currentDate, time.Local) + if err != nil { + return false + } + return prev.Add(24 * time.Hour).Equal(curr) +} diff --git a/modules/player/service/sign_test.go b/modules/player/service/sign_test.go new file mode 100644 index 000000000..f560cb9cd --- /dev/null +++ b/modules/player/service/sign_test.go @@ -0,0 +1,60 @@ +package service + +import ( + "blazing/modules/player/model" + "testing" + + "github.com/pointernil/bitset32" +) + +func TestParseRewardDays(t *testing.T) { + rewards, err := parseRewardDays(`{"days":[{"day":2,"coins":10,"items":[{"item_id":1001,"count":1}],"gift_item_ids":[8]},{"day":1,"gold":2},{"day":2,"coins":5,"items":[{"item_id":1001,"count":2}]}]}`) + if err != nil { + t.Fatalf("parseRewardDays returned error: %v", err) + } + if len(rewards) != 2 { + t.Fatalf("expected 2 reward days, got %d", len(rewards)) + } + if rewards[0].Day != 1 || rewards[0].Gold != 2 { + t.Fatalf("unexpected first reward: %+v", rewards[0]) + } + if rewards[1].Day != 2 { + t.Fatalf("unexpected second reward day: %+v", rewards[1]) + } + if rewards[1].Coins != 15 { + t.Fatalf("expected merged coins to be 15, got %d", rewards[1].Coins) + } + if len(rewards[1].Items) != 1 || rewards[1].Items[0].Count != 3 { + t.Fatalf("expected merged item count to be 3, got %+v", rewards[1].Items) + } +} + +func TestParseRewardDaysMapFormat(t *testing.T) { + rewards, err := parseRewardDays(`{"1":{"coins":1},"3":{"ev_pool":9}}`) + if err != nil { + t.Fatalf("parseRewardDays returned error: %v", err) + } + if len(rewards) != 2 { + t.Fatalf("expected 2 reward days, got %d", len(rewards)) + } + if rewards[0].Day != 1 || rewards[0].Coins != 1 { + t.Fatalf("unexpected day 1 reward: %+v", rewards[0]) + } + if rewards[1].Day != 3 || rewards[1].EVPool != 9 { + t.Fatalf("unexpected day 3 reward: %+v", rewards[1]) + } +} + +func TestNextRewardDay(t *testing.T) { + rewards := []SignRewardDay{{Day: 1}, {Day: 2}, {Day: 3}} + progress := bitset32.New(0).Set(0).Set(1) + record := &model.SignInRecord{SignInProgress: progress.Bytes()} + + day, reward := nextRewardDay(rewards, record) + if reward == nil { + t.Fatal("expected next reward, got nil") + } + if day != 3 || reward.Day != 3 { + t.Fatalf("expected next day to be 3, got day=%d reward=%+v", day, reward) + } +}