feat: 实现每日签到功能并优化战斗和道具逻辑

This commit is contained in:
xinian
2026-04-06 02:06:11 +08:00
committed by cnb
parent f433a26a6d
commit 5b37d9493b
17 changed files with 1066 additions and 86 deletions

View File

@@ -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 重置宠物性格

View File

@@ -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{})
}

View File

@@ -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() {

View File

@@ -27,15 +27,25 @@ const (
// 2. targetIndex 始终表示目标在所属阵营内的槽位。
// 3. targetRelation 用来区分 targetIndex 属于敌方、自己还是队友。
type FightActionEnvelope struct {
// 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"`
}

View File

@@ -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,29 +55,41 @@ type FighterState struct {
// FightStateMeta 是统一状态包的公共元数据。
type FightStateMeta struct {
// 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 我方阵营本次技能结算后的攻击值快照列表。
Left []model.AttackValue `json:"left,omitempty"`
// Right 敌方阵营本次技能结算后的攻击值快照列表。
Right []model.AttackValue `json:"right,omitempty"`
}
// FightLoadState 保存加载进度信息。
type FightLoadState struct {
// UserID 当前上报加载进度的玩家 ID。
UserID uint32 `json:"userId"`
// Percent 当前加载百分比。
Percent uint32 `json:"percent"`
}
// FightChatState 保存战斗内聊天信息。
type FightChatState struct {
// SenderID 发言玩家 ID。
SenderID uint32 `json:"senderId"`
// SenderNickname 发言玩家昵称。
SenderNickname string `json:"senderNickname"`
// Message 聊天内容。
Message string `json:"message"`
}

View File

@@ -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 { // 恢复 panicerr 为 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++ {
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 {
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
}
}
}
}
})

View File

@@ -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() {

View File

@@ -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(),
},
})
}

View File

@@ -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{})
}

View File

@@ -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"},
},
},
}
}

View File

@@ -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),
},
})
}

View File

@@ -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
}

View File

@@ -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
})
}

View File

@@ -17,8 +17,11 @@ type SignInRecord struct {
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"`
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 指定表名(遵循现有规范)

View File

@@ -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"`
}
// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。
type SignIn = configmodel.SignIn
// 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。
func NewSignIn() *configmodel.SignIn {
return configmodel.NewSignIn()
}

View File

@@ -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] = &copyReward
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)
}

View File

@@ -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)
}
}