625 lines
22 KiB
Go
625 lines
22 KiB
Go
package smashegg
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type configBundle struct {
|
|
Config *model.SmashEggConfig
|
|
DrawOptions []model.SmashEggDrawOptionConfig
|
|
Rewards []model.SmashEggRewardConfig
|
|
}
|
|
|
|
// GetConfig returns admin smash egg configuration.
|
|
func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) {
|
|
bundle, err := s.loadConfigBundle(ctx, sysOrigin, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle.Config == nil {
|
|
return defaultConfigResponse(sysOrigin), nil
|
|
}
|
|
return s.buildConfigResponse(ctx, bundle, true)
|
|
}
|
|
|
|
// GetAppConfig returns enabled smash egg configuration for app clients.
|
|
func (s *Service) GetAppConfig(ctx context.Context, user AuthUser) (*ConfigResponse, error) {
|
|
bundle, err := s.loadConfigBundle(ctx, user.SysOrigin, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle.Config == nil || !bundle.Config.Enabled {
|
|
return &ConfigResponse{
|
|
Configured: false,
|
|
SysOrigin: normalizeSysOrigin(user.SysOrigin),
|
|
Timezone: defaultTimezone,
|
|
RTPBasisPoints: defaultRTPBasisPoints,
|
|
RTPPercent: basisPointsPercent(defaultRTPBasisPoints),
|
|
ProbabilityTotal: probabilityTotal,
|
|
NewbieWindowDays: defaultNewbieWindowDays,
|
|
NewbieMaxDrawCount: defaultNewbieMaxDrawCount,
|
|
ActivePoolType: poolTypeGeneral,
|
|
DrawOptions: []DrawOptionPayload{},
|
|
Rewards: []RewardConfigPayload{},
|
|
}, nil
|
|
}
|
|
resp, err := s.buildConfigResponse(ctx, bundle, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
poolType, rewards, eligibility, err := s.resolveRewardPool(ctx, bundle, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
options := make([]DrawOptionPayload, 0, len(bundle.DrawOptions))
|
|
for _, row := range bundle.DrawOptions {
|
|
expectedValue := expectedRewardValueForOption(bundle.Config.RTPBasisPoints, row, rewards)
|
|
options = append(options, drawOptionPayload(row, expectedValue))
|
|
}
|
|
resp.ActivePoolType = poolType
|
|
resp.NewbieEligible = eligibility.Eligible
|
|
resp.NewbieRemainingDrawCount = eligibility.Remaining
|
|
resp.DrawOptions = options
|
|
resp.Rewards = rewardPayloadsFromConfig(rewards)
|
|
resp.ExpectedValueGold = amount2(expectedRewardValue(rewards))
|
|
resp.NewbieRewards = nil
|
|
return resp, nil
|
|
}
|
|
|
|
// SaveConfig saves admin smash egg configuration.
|
|
func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
|
normalized, err := normalizeSaveRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now()
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var configRow model.SmashEggConfig
|
|
if err := tx.Where("sys_origin = ?", normalized.SysOrigin).First(&configRow).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
|
nextID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
configRow = model.SmashEggConfig{
|
|
ID: nextID,
|
|
SysOrigin: normalized.SysOrigin,
|
|
CreateTime: now,
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
configRow.Enabled = normalized.Enabled
|
|
configRow.Timezone = normalized.Timezone
|
|
configRow.RTPBasisPoints = normalized.RTPBasisPoints
|
|
configRow.NewbiePoolEnabled = normalized.NewbiePoolEnabled
|
|
configRow.NewbieWindowDays = normalized.NewbieWindowDays
|
|
configRow.NewbieMaxDrawCount = normalized.NewbieMaxDrawCount
|
|
configRow.NewbieMinRechargeAmount = normalized.NewbieMinRechargeAmount
|
|
configRow.UpdateTime = now
|
|
if configRow.CreateTime.IsZero() {
|
|
configRow.CreateTime = now
|
|
}
|
|
if err := tx.Save(&configRow).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.SmashEggDrawOptionConfig{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.SmashEggRewardConfig{}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
optionRows := make([]model.SmashEggDrawOptionConfig, 0, len(normalized.DrawOptions))
|
|
for index, item := range normalized.DrawOptions {
|
|
nextID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
sortValue := item.Sort
|
|
if sortValue <= 0 {
|
|
sortValue = index + 1
|
|
}
|
|
optionRows = append(optionRows, model.SmashEggDrawOptionConfig{
|
|
ID: nextID,
|
|
ConfigID: configRow.ID,
|
|
SysOrigin: normalized.SysOrigin,
|
|
OptionKey: item.OptionKey,
|
|
Label: strings.TrimSpace(item.Label),
|
|
Times: item.Times,
|
|
PriceGold: item.PriceGold,
|
|
Sort: sortValue,
|
|
Enabled: item.Enabled,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
})
|
|
}
|
|
if len(optionRows) > 0 {
|
|
if err := tx.Create(&optionRows).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
allRewards := make([]RewardConfigInput, 0, len(normalized.Rewards)+len(normalized.NewbieRewards))
|
|
allRewards = append(allRewards, normalized.Rewards...)
|
|
allRewards = append(allRewards, normalized.NewbieRewards...)
|
|
rewardRows := make([]model.SmashEggRewardConfig, 0, len(allRewards))
|
|
for index, item := range allRewards {
|
|
nextID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
sortValue := item.Sort
|
|
if sortValue <= 0 {
|
|
sortValue = index + 1
|
|
}
|
|
rewardRows = append(rewardRows, model.SmashEggRewardConfig{
|
|
ID: nextID,
|
|
ConfigID: configRow.ID,
|
|
SysOrigin: normalized.SysOrigin,
|
|
PoolType: normalizePoolType(item.PoolType),
|
|
RewardType: item.RewardType,
|
|
RewardGroupID: item.RewardGroupID,
|
|
RewardGroupName: strings.TrimSpace(item.RewardGroupName),
|
|
ResourceType: strings.TrimSpace(item.ResourceType),
|
|
ResourceURL: strings.TrimSpace(item.ResourceURL),
|
|
CoverURL: strings.TrimSpace(item.CoverURL),
|
|
AnimationURL: strings.TrimSpace(item.AnimationURL),
|
|
DurationDays: item.DurationDays,
|
|
DisplayGoldAmount: item.DisplayGoldAmount,
|
|
GoldAmount: item.GoldAmount,
|
|
RewardValueGold: item.RewardValueGold,
|
|
Probability: item.Probability,
|
|
Sort: sortValue,
|
|
Enabled: item.Enabled,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
})
|
|
}
|
|
if len(rewardRows) > 0 {
|
|
return tx.Create(&rewardRows).Error
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetConfig(ctx, normalized.SysOrigin)
|
|
}
|
|
|
|
func normalizeSaveRequest(req SaveConfigRequest) (SaveConfigRequest, error) {
|
|
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
if sysOrigin == "" {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
|
|
}
|
|
timezone := normalizeTimezone(req.Timezone)
|
|
if _, err := time.LoadLocation(timezone); err != nil {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
|
|
}
|
|
|
|
rtp := req.RTPBasisPoints
|
|
if rtp <= 0 {
|
|
rtp = defaultRTPBasisPoints
|
|
}
|
|
if rtp > 100000 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_rtp", "rtpBasisPoints must be less than or equal to 100000")
|
|
}
|
|
|
|
options := append([]DrawOptionInput(nil), req.DrawOptions...)
|
|
if len(options) == 0 {
|
|
options = defaultDrawOptionInputs()
|
|
}
|
|
sortOptionInputs(options)
|
|
enabledOptionCount := 0
|
|
seenTimes := map[int]struct{}{}
|
|
for index := range options {
|
|
item := &options[index]
|
|
item.OptionKey = normalizeOptionKey(item.OptionKey, index)
|
|
item.Label = strings.TrimSpace(item.Label)
|
|
if item.Label == "" {
|
|
item.Label = item.OptionKey
|
|
}
|
|
if item.Sort <= 0 {
|
|
item.Sort = index + 1
|
|
}
|
|
if item.Times <= 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_draw_times", "draw option times must be greater than 0")
|
|
}
|
|
if item.PriceGold <= 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_draw_price", "draw option priceGold must be greater than 0")
|
|
}
|
|
if _, exists := seenTimes[item.Times]; exists {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "duplicate_draw_times", "draw option times must be unique")
|
|
}
|
|
seenTimes[item.Times] = struct{}{}
|
|
if item.Enabled {
|
|
enabledOptionCount++
|
|
}
|
|
}
|
|
|
|
rewards, enabledRewardCount, err := normalizeRewardInputs(req.Rewards, poolTypeGeneral)
|
|
if err != nil {
|
|
return SaveConfigRequest{}, err
|
|
}
|
|
newbieRewards, newbieEnabledRewardCount, err := normalizeRewardInputs(req.NewbieRewards, poolTypeNewbie)
|
|
if err != nil {
|
|
return SaveConfigRequest{}, err
|
|
}
|
|
|
|
newbieWindowDays := req.NewbieWindowDays
|
|
if newbieWindowDays <= 0 {
|
|
newbieWindowDays = defaultNewbieWindowDays
|
|
}
|
|
if newbieWindowDays > 90 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_newbie_window_days", "newbieWindowDays must be less than or equal to 90")
|
|
}
|
|
newbieMaxDrawCount := req.NewbieMaxDrawCount
|
|
if req.NewbiePoolEnabled && newbieMaxDrawCount <= 0 {
|
|
newbieMaxDrawCount = defaultNewbieMaxDrawCount
|
|
}
|
|
if newbieMaxDrawCount < 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_newbie_max_draw_count", "newbieMaxDrawCount must be greater than or equal to 0")
|
|
}
|
|
if req.NewbieMinRechargeAmount < 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_newbie_min_recharge_amount", "newbieMinRechargeAmount must be greater than or equal to 0")
|
|
}
|
|
if req.Enabled && enabledOptionCount == 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "empty_draw_options", "enabled smash egg requires at least one enabled draw option")
|
|
}
|
|
if req.Enabled && enabledRewardCount == 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "empty_rewards", "enabled smash egg requires at least one enabled reward")
|
|
}
|
|
if enabledOptionCount > 0 && enabledRewardCount > 0 {
|
|
rewards, err = applyAutoProbabilitiesToInputs(rtp, options, rewards, "general")
|
|
if err != nil {
|
|
return SaveConfigRequest{}, err
|
|
}
|
|
}
|
|
if req.Enabled && req.NewbiePoolEnabled && newbieEnabledRewardCount == 0 {
|
|
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "empty_newbie_rewards", "enabled newbie pool requires at least one enabled reward")
|
|
}
|
|
if req.NewbiePoolEnabled && enabledOptionCount > 0 && newbieEnabledRewardCount > 0 {
|
|
newbieRewards, err = applyAutoProbabilitiesToInputs(rtp, options, newbieRewards, "newbie")
|
|
if err != nil {
|
|
return SaveConfigRequest{}, err
|
|
}
|
|
}
|
|
|
|
return SaveConfigRequest{
|
|
SysOrigin: sysOrigin,
|
|
Enabled: req.Enabled,
|
|
Timezone: timezone,
|
|
RTPBasisPoints: rtp,
|
|
NewbiePoolEnabled: req.NewbiePoolEnabled,
|
|
NewbieWindowDays: newbieWindowDays,
|
|
NewbieMaxDrawCount: newbieMaxDrawCount,
|
|
NewbieMinRechargeAmount: req.NewbieMinRechargeAmount,
|
|
DrawOptions: options,
|
|
Rewards: rewards,
|
|
NewbieRewards: newbieRewards,
|
|
}, nil
|
|
}
|
|
|
|
func normalizeRewardInputs(input []RewardConfigInput, poolType string) ([]RewardConfigInput, int, error) {
|
|
rewards := append([]RewardConfigInput(nil), input...)
|
|
sortRewardInputs(rewards)
|
|
enabledRewardCount := 0
|
|
for index := range rewards {
|
|
item := &rewards[index]
|
|
item.PoolType = normalizePoolType(poolType)
|
|
item.RewardType = normalizeRewardType(item.RewardType)
|
|
if item.RewardType == "" {
|
|
item.RewardType = rewardTypeGold
|
|
}
|
|
item.ResourceType = strings.ToUpper(strings.TrimSpace(item.ResourceType))
|
|
item.ResourceName = strings.TrimSpace(firstNonEmpty(item.ResourceName, item.RewardGroupName))
|
|
item.ResourceURL = strings.TrimSpace(item.ResourceURL)
|
|
item.RewardGroupName = strings.TrimSpace(item.RewardGroupName)
|
|
item.CoverURL = strings.TrimSpace(item.CoverURL)
|
|
item.AnimationURL = strings.TrimSpace(item.AnimationURL)
|
|
if item.Sort <= 0 {
|
|
item.Sort = index + 1
|
|
}
|
|
|
|
switch item.RewardType {
|
|
case rewardTypeGold:
|
|
if item.GoldAmount <= 0 {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_gold_amount", "gold reward requires goldAmount")
|
|
}
|
|
item.ResourceID = 0
|
|
item.ResourceType = ""
|
|
item.ResourceName = ""
|
|
item.ResourceURL = ""
|
|
item.RewardGroupID = nil
|
|
item.RewardGroupName = "金币"
|
|
item.DurationDays = 0
|
|
item.DisplayGoldAmount = 0
|
|
item.RewardValueGold = item.GoldAmount
|
|
case rewardTypeResource:
|
|
if item.ResourceID.Int64() <= 0 && item.RewardGroupID != nil {
|
|
item.ResourceID = ResourceID(*item.RewardGroupID)
|
|
}
|
|
if item.ResourceID.Int64() <= 0 {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_resource", "resource reward requires resourceId")
|
|
}
|
|
if item.ResourceType == "" {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_resource_type", "resource reward requires resourceType")
|
|
}
|
|
if item.DurationDays <= 0 {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_duration_days", "resource reward requires durationDays greater than 0")
|
|
}
|
|
if item.DisplayGoldAmount <= 0 {
|
|
if item.RewardValueGold > 0 {
|
|
item.DisplayGoldAmount = item.RewardValueGold
|
|
} else {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_display_gold_amount", "resource reward displayGoldAmount must be greater than 0")
|
|
}
|
|
}
|
|
item.RewardGroupID = resourceIDPtr(item.ResourceID)
|
|
if item.ResourceName == "" {
|
|
item.ResourceName = "资源 " + strconv.FormatInt(item.ResourceID.Int64(), 10)
|
|
}
|
|
item.RewardGroupName = item.ResourceName
|
|
item.GoldAmount = 0
|
|
item.RewardValueGold = item.DisplayGoldAmount
|
|
if item.AnimationURL == "" {
|
|
item.AnimationURL = item.ResourceURL
|
|
}
|
|
default:
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_reward_type", "rewardType must be RESOURCE or GOLD")
|
|
}
|
|
|
|
if item.RewardValueGold <= 0 {
|
|
return nil, 0, NewAppError(http.StatusBadRequest, "invalid_reward_value", "reward value must be greater than 0")
|
|
}
|
|
item.Probability = 0
|
|
if item.Enabled {
|
|
enabledRewardCount++
|
|
}
|
|
}
|
|
return rewards, enabledRewardCount, nil
|
|
}
|
|
|
|
func weightedRewardValueFromInputs(rewards []RewardConfigInput) int64 {
|
|
var total int64
|
|
for _, reward := range rewards {
|
|
if !reward.Enabled || reward.Probability <= 0 {
|
|
continue
|
|
}
|
|
total += reward.RewardValueGold * int64(reward.Probability)
|
|
}
|
|
return total
|
|
}
|
|
|
|
func drawOptionRTPBasisPointsFromWeightedValue(weightedValue int64, times int, priceGold int64) int {
|
|
if weightedValue <= 0 || times <= 0 || priceGold <= 0 {
|
|
return 0
|
|
}
|
|
numerator := weightedValue * int64(times) * 10000
|
|
denominator := priceGold * int64(probabilityTotal)
|
|
return int(roundDiv(numerator, denominator))
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func effectiveNewbieWindowDays(value int) int {
|
|
if value <= 0 {
|
|
return defaultNewbieWindowDays
|
|
}
|
|
return value
|
|
}
|
|
|
|
func effectiveNewbieMaxDrawCount(value int, enabled bool) int {
|
|
if value <= 0 && enabled {
|
|
return defaultNewbieMaxDrawCount
|
|
}
|
|
return value
|
|
}
|
|
|
|
func (s *Service) loadConfigBundle(ctx context.Context, sysOrigin string, onlyEnabled bool) (*configBundle, error) {
|
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
if sysOrigin == "" {
|
|
return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
|
|
}
|
|
|
|
var configRow model.SmashEggConfig
|
|
err := s.db.WithContext(ctx).Where("sys_origin = ?", sysOrigin).First(&configRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &configBundle{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
optionQuery := s.db.WithContext(ctx).Where("config_id = ?", configRow.ID).Order("sort asc, id asc")
|
|
rewardQuery := s.db.WithContext(ctx).Where("config_id = ?", configRow.ID).Order("sort asc, id asc")
|
|
if onlyEnabled {
|
|
optionQuery = optionQuery.Where("enabled = ?", true)
|
|
rewardQuery = rewardQuery.Where("enabled = ?", true)
|
|
}
|
|
|
|
var options []model.SmashEggDrawOptionConfig
|
|
if err := optionQuery.Find(&options).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var rewards []model.SmashEggRewardConfig
|
|
if err := rewardQuery.Find(&rewards).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &configBundle{Config: &configRow, DrawOptions: options, Rewards: rewards}, nil
|
|
}
|
|
|
|
func defaultConfigResponse(sysOrigin string) *ConfigResponse {
|
|
return &ConfigResponse{
|
|
Configured: false,
|
|
SysOrigin: normalizeSysOrigin(sysOrigin),
|
|
Enabled: false,
|
|
Timezone: defaultTimezone,
|
|
RTPBasisPoints: defaultRTPBasisPoints,
|
|
RTPPercent: basisPointsPercent(defaultRTPBasisPoints),
|
|
ProbabilityTotal: probabilityTotal,
|
|
ExpectedValueGold: "0.00",
|
|
NewbieWindowDays: defaultNewbieWindowDays,
|
|
NewbieMaxDrawCount: defaultNewbieMaxDrawCount,
|
|
ActivePoolType: poolTypeGeneral,
|
|
DrawOptions: defaultDrawOptionPayloads(0),
|
|
Rewards: []RewardConfigPayload{},
|
|
NewbieRewards: []RewardConfigPayload{},
|
|
}
|
|
}
|
|
|
|
func (s *Service) buildConfigResponse(ctx context.Context, bundle *configBundle, includeRewardItems bool) (*ConfigResponse, error) {
|
|
configRow := bundle.Config
|
|
generalRewards := rewardsForPool(bundle.Rewards, poolTypeGeneral)
|
|
newbieRewards := rewardsForPool(bundle.Rewards, poolTypeNewbie)
|
|
expectedValue := expectedRewardValue(generalRewards)
|
|
newbieExpectedValue := expectedRewardValue(newbieRewards)
|
|
|
|
options := make([]DrawOptionPayload, 0, len(bundle.DrawOptions))
|
|
for _, row := range bundle.DrawOptions {
|
|
optionExpectedValue := expectedRewardValueForOption(configRow.RTPBasisPoints, row, generalRewards)
|
|
options = append(options, drawOptionPayload(row, optionExpectedValue))
|
|
}
|
|
rewards := rewardPayloadsFromConfig(generalRewards)
|
|
newbieRewardPayloads := rewardPayloadsFromConfig(newbieRewards)
|
|
|
|
return &ConfigResponse{
|
|
Configured: true,
|
|
ID: configRow.ID,
|
|
SysOrigin: configRow.SysOrigin,
|
|
Enabled: configRow.Enabled,
|
|
Timezone: normalizeTimezone(configRow.Timezone),
|
|
RTPBasisPoints: configRow.RTPBasisPoints,
|
|
RTPPercent: basisPointsPercent(configRow.RTPBasisPoints),
|
|
ProbabilityTotal: probabilityTotal,
|
|
ExpectedValueGold: amount2(expectedValue),
|
|
NewbiePoolEnabled: configRow.NewbiePoolEnabled,
|
|
NewbieWindowDays: effectiveNewbieWindowDays(configRow.NewbieWindowDays),
|
|
NewbieMaxDrawCount: effectiveNewbieMaxDrawCount(configRow.NewbieMaxDrawCount, configRow.NewbiePoolEnabled),
|
|
NewbieMinRechargeAmount: configRow.NewbieMinRechargeAmount,
|
|
NewbieExpectedValueGold: amount2(newbieExpectedValue),
|
|
ActivePoolType: poolTypeGeneral,
|
|
DrawOptions: options,
|
|
Rewards: rewards,
|
|
NewbieRewards: newbieRewardPayloads,
|
|
UpdateTime: formatDateTime(configRow.UpdateTime),
|
|
}, nil
|
|
}
|
|
|
|
func rewardPayloadsFromConfig(rewards []model.SmashEggRewardConfig) []RewardConfigPayload {
|
|
payloads := make([]RewardConfigPayload, 0, len(rewards))
|
|
for _, row := range rewards {
|
|
payloads = append(payloads, rewardPayloadFromConfig(row))
|
|
}
|
|
return payloads
|
|
}
|
|
|
|
func drawOptionPayload(row model.SmashEggDrawOptionConfig, expectedValuePerDraw float64) DrawOptionPayload {
|
|
rtp := 0
|
|
if row.PriceGold > 0 {
|
|
rtp = int((expectedValuePerDraw * float64(row.Times) / float64(row.PriceGold) * 10000) + 0.5)
|
|
}
|
|
return DrawOptionPayload{
|
|
ID: row.ID,
|
|
Enabled: row.Enabled,
|
|
Sort: row.Sort,
|
|
OptionKey: row.OptionKey,
|
|
Label: row.Label,
|
|
Times: row.Times,
|
|
PriceGold: row.PriceGold,
|
|
RTPBasisPoints: rtp,
|
|
RTPPercent: basisPointsPercent(rtp),
|
|
ExpectedValueGold: amount2(expectedValuePerDraw * float64(row.Times)),
|
|
}
|
|
}
|
|
|
|
func rewardPayloadFromConfig(row model.SmashEggRewardConfig) RewardConfigPayload {
|
|
return RewardConfigPayload{
|
|
ID: row.ID,
|
|
Enabled: row.Enabled,
|
|
Sort: row.Sort,
|
|
PoolType: normalizePoolType(row.PoolType),
|
|
RewardType: row.RewardType,
|
|
ResourceID: ResourceID(int64PtrValue(row.RewardGroupID)),
|
|
ResourceType: row.ResourceType,
|
|
ResourceName: row.RewardGroupName,
|
|
ResourceURL: row.ResourceURL,
|
|
RewardGroupID: row.RewardGroupID,
|
|
RewardGroupName: row.RewardGroupName,
|
|
CoverURL: row.CoverURL,
|
|
AnimationURL: row.AnimationURL,
|
|
DurationDays: row.DurationDays,
|
|
DisplayGoldAmount: row.DisplayGoldAmount,
|
|
GoldAmount: row.GoldAmount,
|
|
RewardValueGold: row.RewardValueGold,
|
|
Probability: row.Probability,
|
|
ProbabilityPercent: probabilityPercent(row.Probability),
|
|
}
|
|
}
|
|
|
|
func rewardsForPool(rewards []model.SmashEggRewardConfig, poolType string) []model.SmashEggRewardConfig {
|
|
poolType = normalizePoolType(poolType)
|
|
result := make([]model.SmashEggRewardConfig, 0, len(rewards))
|
|
for _, reward := range rewards {
|
|
reward.RewardType = normalizeRewardType(reward.RewardType)
|
|
if normalizePoolType(reward.PoolType) == poolType {
|
|
result = append(result, reward)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func expectedRewardValue(rewards []model.SmashEggRewardConfig) float64 {
|
|
var total float64
|
|
for _, reward := range rewards {
|
|
if !reward.Enabled || reward.Probability <= 0 {
|
|
continue
|
|
}
|
|
total += float64(reward.RewardValueGold) * float64(reward.Probability) / probabilityTotal
|
|
}
|
|
return total
|
|
}
|
|
|
|
func defaultDrawOptionInputs() []DrawOptionInput {
|
|
return []DrawOptionInput{
|
|
{Enabled: true, Sort: 1, OptionKey: optionKeyOne, Label: "Smash 1", Times: 1, PriceGold: 500},
|
|
{Enabled: true, Sort: 2, OptionKey: optionKeyTen, Label: "Smash 10", Times: 10, PriceGold: 5000},
|
|
{Enabled: true, Sort: 3, OptionKey: optionKeyHundred, Label: "Smash 100", Times: 100, PriceGold: 50000},
|
|
}
|
|
}
|
|
|
|
func defaultDrawOptionPayloads(expectedValuePerDraw float64) []DrawOptionPayload {
|
|
defaults := defaultDrawOptionInputs()
|
|
rows := make([]DrawOptionPayload, 0, len(defaults))
|
|
for _, item := range defaults {
|
|
rows = append(rows, drawOptionPayload(model.SmashEggDrawOptionConfig{
|
|
Enabled: item.Enabled,
|
|
Sort: item.Sort,
|
|
OptionKey: item.OptionKey,
|
|
Label: item.Label,
|
|
Times: item.Times,
|
|
PriceGold: item.PriceGold,
|
|
}, expectedValuePerDraw))
|
|
}
|
|
return rows
|
|
}
|