Improve live and random live handling

- Add random live handlers, schemas and persistence for lot/play/continue/gameover/reward flow
- Update live status and schedule generation, including special rotation handling and CN timezone alignment
- Add config support for unlocking all daily special rotation songs and document the option
- Refresh live play, reward, precise score and session logic plus bundled main.db updates

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-06 20:59:45 +08:00
parent 4e642cc7c1
commit 0aacf8a4e9
28 changed files with 1085 additions and 228 deletions
+163 -51
View File
@@ -2,6 +2,7 @@ package live
import (
"fmt"
"honoka-chan/config"
liveapischema "honoka-chan/internal/schema/api/live"
"honoka-chan/internal/session"
"os"
@@ -15,7 +16,7 @@ const (
randomLiveEndDate = "2038-01-19 12:14:07"
)
var jst = time.FixedZone("JST", 9*3600)
var cst = time.FixedZone("CST", 8*3600)
type liveAvailabilityRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"`
@@ -33,6 +34,12 @@ type specialLiveRotationRow struct {
BaseDate string `xorm:"base_date"`
}
type specialLiveRotationSchedule struct {
LiveDifficultyID int
StartTime time.Time
EndTime time.Time
}
func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
rows, err := listAvailableNormalLiveRows(ss)
if err != nil {
@@ -46,25 +53,6 @@ func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
return result, nil
}
func listAvailableNormalLiveTrackIDs(ss *session.Session) ([]int, error) {
rows, err := listAvailableNormalLiveRows(ss)
if err != nil {
return nil, err
}
trackSet := map[int]struct{}{}
for _, row := range rows {
trackSet[row.LiveTrackID] = struct{}{}
}
result := make([]int, 0, len(trackSet))
for id := range trackSet {
result = append(result, id)
}
sort.Ints(result)
return result, nil
}
func listAvailableNormalLiveRows(ss *session.Session) ([]liveAvailabilityRow, error) {
rows := []liveAvailabilityRow{}
err := ss.MainEng.Table("normal_live_m").Alias("live").
@@ -155,11 +143,11 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
dayModulo int64
}{
liveDifficultyID: row.LiveDifficultyID,
dayModulo: baseDate.Unix() / 86400,
dayModulo: dayIndexInCST(baseDate),
})
}
currentDay := now.In(jst).Unix() / 86400
currentDay := dayIndexInCST(now)
groupIDs := make([]int, 0, len(grouped))
for groupID := range grouped {
groupIDs = append(groupIDs, groupID)
@@ -183,54 +171,174 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
return result, nil
}
func listAllSpecialRotationDifficultyIDs(ss *session.Session) (map[int]struct{}, error) {
ids := []int{}
func listCurrentAndNextSpecialRotationSchedules(ss *session.Session, now time.Time) ([]specialLiveRotationSchedule, error) {
if config.Conf.Settings.UnlockAllSpecialRotation {
return listAllSpecialRotationSchedules(ss)
}
rows := []specialLiveRotationRow{}
err := ss.MainEng.Table("special_live_rotation_m").
Cols("live_difficulty_id").
Find(&ids)
OrderBy("rotation_group_id ASC, base_date ASC").
Find(&rows)
if err != nil {
return nil, err
}
result := make(map[int]struct{}, len(ids))
for _, id := range ids {
result[id] = struct{}{}
type rotationEntry struct {
liveDifficultyID int
baseTime time.Time
dayModulo int64
}
grouped := map[int][]rotationEntry{}
groupIDs := []int{}
for _, row := range rows {
baseDate, err := parseRotationBaseDate(row.BaseDate)
if err != nil {
return nil, err
}
if _, ok := grouped[row.RotationGroupID]; !ok {
groupIDs = append(groupIDs, row.RotationGroupID)
}
grouped[row.RotationGroupID] = append(grouped[row.RotationGroupID], rotationEntry{
liveDifficultyID: row.LiveDifficultyID,
baseTime: baseDate,
dayModulo: dayIndexInCST(baseDate),
})
}
sort.Ints(groupIDs)
currentDay := dayIndexInCST(now)
result := make([]specialLiveRotationSchedule, 0, len(groupIDs)*2)
for _, groupID := range groupIDs {
liveList := grouped[groupID]
if len(liveList) == 0 {
continue
}
currentIndex := -1
currentDayModulo := currentDay % int64(len(liveList))
for i, live := range liveList {
if live.dayModulo%int64(len(liveList)) == currentDayModulo {
currentIndex = i
break
}
}
if currentIndex == -1 {
continue
}
periodDays := int64(1)
if len(liveList) >= 2 {
diff := int64(liveList[1].baseTime.Sub(liveList[0].baseTime) / (24 * time.Hour))
if diff > 0 {
periodDays = diff
}
}
currentEntry := liveList[currentIndex]
offsetDays := (currentDay - currentEntry.dayModulo) % periodDays
if offsetDays < 0 {
offsetDays += periodDays
}
currentStartTime := startOfDay(now.In(cst)).AddDate(0, 0, -int(offsetDays))
nextStartTime := currentStartTime.AddDate(0, 0, int(periodDays))
duration := time.Duration(periodDays)*24*time.Hour - time.Second
nextIndex := (currentIndex + 1) % len(liveList)
for i, live := range liveList {
switch i {
case currentIndex:
result = append(result, specialLiveRotationSchedule{
LiveDifficultyID: live.liveDifficultyID,
StartTime: currentStartTime,
EndTime: currentStartTime.Add(duration),
})
case nextIndex:
result = append(result, specialLiveRotationSchedule{
LiveDifficultyID: live.liveDifficultyID,
StartTime: nextStartTime,
EndTime: nextStartTime.Add(duration),
})
}
}
}
return result, nil
}
func listAllSpecialRotationSchedules(ss *session.Session) ([]specialLiveRotationSchedule, error) {
rows := []specialLiveRotationRow{}
err := ss.MainEng.Table("special_live_rotation_m").
OrderBy("rotation_group_id ASC, base_date ASC").
Find(&rows)
if err != nil {
return nil, err
}
type rotationEntry struct {
liveDifficultyID int
baseTime time.Time
}
grouped := map[int][]rotationEntry{}
groupIDs := []int{}
for _, row := range rows {
baseDate, err := parseRotationBaseDate(row.BaseDate)
if err != nil {
return nil, err
}
if _, ok := grouped[row.RotationGroupID]; !ok {
groupIDs = append(groupIDs, row.RotationGroupID)
}
grouped[row.RotationGroupID] = append(grouped[row.RotationGroupID], rotationEntry{
liveDifficultyID: row.LiveDifficultyID,
baseTime: baseDate,
})
}
sort.Ints(groupIDs)
result := make([]specialLiveRotationSchedule, 0, len(rows))
for _, groupID := range groupIDs {
liveList := grouped[groupID]
if len(liveList) == 0 {
continue
}
periodDays := int64(1)
if len(liveList) >= 2 {
diff := int64(liveList[1].baseTime.Sub(liveList[0].baseTime) / (24 * time.Hour))
if diff > 0 {
periodDays = diff
}
}
if periodDays != 1 {
continue
}
for _, live := range liveList {
result = append(result, specialLiveRotationSchedule{
LiveDifficultyID: live.liveDifficultyID,
StartTime: time.Unix(0, 0).In(cst),
EndTime: time.Unix(2147483647, 0).In(cst),
})
}
}
return result, nil
}
func listAvailableTrainingLiveDifficultyIDs(ss *session.Session) ([]int, error) {
normalTrackIDs, err := listAvailableNormalLiveTrackIDs(ss)
if err != nil {
return nil, err
}
specialRows, err := listAvailableSpecialLiveRows(ss)
if err != nil {
return nil, err
}
rotationIDs, err := listAllSpecialRotationDifficultyIDs(ss)
if err != nil {
return nil, err
}
normalTrackIDSet := make(map[int]struct{}, len(normalTrackIDs))
for _, id := range normalTrackIDs {
normalTrackIDSet[id] = struct{}{}
}
result := make([]int, 0)
for _, row := range specialRows {
if _, ok := normalTrackIDSet[row.LiveTrackID]; !ok {
continue
}
if row.Difficulty <= 5 || row.AcFlag != 0 || row.ExcludeClearCountFlag != 0 {
continue
}
if _, ok := rotationIDs[row.LiveDifficultyID]; ok {
continue
}
result = append(result, row.LiveDifficultyID)
}
@@ -244,7 +352,7 @@ func parseRotationBaseDate(value string) (time.Time, error) {
"2006/01/02 3:04:05",
}
for _, layout := range layouts {
t, err := time.ParseInLocation(layout, value, jst)
t, err := time.ParseInLocation(layout, value, cst)
if err == nil {
return t, nil
}
@@ -256,6 +364,10 @@ func startOfDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
func dayIndexInCST(t time.Time) int64 {
return startOfDay(t.In(cst)).Unix() / 86400
}
func nextDayStart(t time.Time) time.Time {
return startOfDay(t).Add(24 * time.Hour)
}
+6 -9
View File
@@ -11,20 +11,17 @@ import (
func liveSchedule(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx)
now := time.Now()
liveIDs, err := listTodaySpecialRotationDifficultyIDs(ss, now)
schedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now())
if err != nil {
return nil, err
}
liveList := make([]liveapischema.LiveList, 0, len(liveIDs))
startDate := startOfDay(now.In(jst)).Format("2006-01-02 15:04:05")
endDate := nextDayStart(now.In(jst)).Format("2006-01-02 15:04:05")
for _, id := range liveIDs {
liveList := make([]liveapischema.LiveList, 0, len(schedules))
for _, schedule := range schedules {
liveList = append(liveList, liveapischema.LiveList{
LiveDifficultyID: id,
StartDate: startDate,
EndDate: endDate,
LiveDifficultyID: schedule.LiveDifficultyID,
StartDate: schedule.StartTime.Format("2006-01-02 15:04:05"),
EndDate: schedule.EndTime.Format("2006-01-02 15:04:05"),
IsRandom: false,
})
}
+5 -1
View File
@@ -16,10 +16,14 @@ func liveStatus(ctx *gin.Context) (res any, err error) {
return nil, err
}
specialIDs, err := listTodaySpecialRotationDifficultyIDs(ss, time.Now())
specialSchedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now())
if err != nil {
return nil, err
}
specialIDs := make([]int, 0, len(specialSchedules))
for _, schedule := range specialSchedules {
specialIDs = append(specialIDs, schedule.LiveDifficultyID)
}
trainingIDs, err := listAvailableTrainingLiveDifficultyIDs(ss)
if err != nil {
return nil, err
+1
View File
@@ -26,6 +26,7 @@ import (
_ "honoka-chan/internal/handler/profile"
_ "honoka-chan/internal/handler/ranking"
_ "honoka-chan/internal/handler/reward"
_ "honoka-chan/internal/handler/rlive"
_ "honoka-chan/internal/handler/scenario"
_ "honoka-chan/internal/handler/secretbox"
_ "honoka-chan/internal/handler/subscenario"
+6 -2
View File
@@ -14,7 +14,11 @@ func continuee(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
ss.Respond(liveschema.ContinueResp{
ss.Respond(BuildContinueResp(ss))
}
func BuildContinueResp(ss *session.Session) liveschema.ContinueResp {
return liveschema.ContinueResp{
ResponseData: liveschema.ContinueData{
BeforeSnsCoin: ss.UserPref.SnsCoin,
AfterSnsCoin: ss.UserPref.SnsCoin,
@@ -22,7 +26,7 @@ func continuee(ctx *gin.Context) {
},
ReleaseInfo: []any{},
StatusCode: 200,
})
}
}
func init() {
+9 -3
View File
@@ -12,15 +12,21 @@ import (
func gameOver(ctx *gin.Context) {
ss := session.Get(ctx)
defer func() {
ss.ClearLiveInProgress()
if ss.UserEng != nil {
ss.ClearLiveInProgress()
}
ss.Finalize()
}()
ss.Respond(liveschema.GameOverResp{
ss.Respond(BuildGameOverResp())
}
func BuildGameOverResp() liveschema.GameOverResp {
return liveschema.GameOverResp{
ResponseData: []any{},
ReleaseInfo: []any{},
StatusCode: 200,
})
}
}
func init() {
+29 -20
View File
@@ -14,24 +14,37 @@ import (
func partyList(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
userInfo := ss.GetUserInfo()
pref := usermodel.UserPref{}
_, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
partyListData, err := BuildPartyListData(ss)
if ss.CheckErr(err) {
return
}
ss.Respond(liveschema.PartyListResp{
ResponseData: partyListData,
ReleaseInfo: []any{},
StatusCode: 200,
})
}
func BuildPartyListData(ss *session.Session) (liveschema.PartyListData, error) {
userInfo := ss.GetUserInfo()
pref := usermodel.UserPref{}
_, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if err != nil {
return liveschema.PartyListData{}, err
}
supportRows, err := listLiveSupportRows(ss)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PartyListData{}, err
}
partyList := make([]liveschema.PartyList, 0, len(supportRows))
for _, row := range supportRows {
party, err := buildLiveSupportParty(ss, row)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PartyListData{}, err
}
partyList = append(partyList, party)
}
@@ -44,22 +57,18 @@ func partyList(ctx *gin.Context) {
}
selfParty.AvailableSocialPoint = 10
selfParty.CenterUnitInfo, err = mustFindCenterUnitInfo(ss, ss.UserID, pref.UnitOwningUserID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PartyListData{}, err
}
partyList = append(partyList, selfParty)
}
ss.Respond(liveschema.PartyListResp{
ResponseData: liveschema.PartyListData{
PartyList: partyList,
TrainingEnergy: userInfo.TrainingEnergy,
TrainingEnergyMax: userInfo.TrainingEnergyMax,
ServerTimestamp: time.Now().Unix(),
},
ReleaseInfo: []any{},
StatusCode: 200,
})
return liveschema.PartyListData{
PartyList: partyList,
TrainingEnergy: userInfo.TrainingEnergy,
TrainingEnergyMax: userInfo.TrainingEnergyMax,
ServerTimestamp: time.Now().Unix(),
}, nil
}
func init() {
+108 -116
View File
@@ -25,93 +25,104 @@ func play(ctx *gin.Context) {
}
// fmt.Println(ctx.MustGet("request_data").(string))
if err := ss.ResetRandomLiveInProgress(); ss.CheckErr(err) {
return
}
ss.RegisterLiveInProgress(playReq.UnitDeckID)
difficultyID, err := strconv.Atoi(playReq.LiveDifficultyID)
playResp, err := BuildPlayResp(ss, playReq, false)
if ss.CheckErr(err) {
return
}
ss.Respond(playResp)
}
func BuildPlayResp(ss *session.Session, playReq liveschema.PlayReq, isRandom bool) (liveschema.PlayResp, error) {
difficultyID, err := strconv.Atoi(playReq.LiveDifficultyID)
if err != nil {
return liveschema.PlayResp{}, err
}
liveSetting, err := loadLiveSetting(ss, difficultyID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
notes, err := loadLiveNotes(liveSetting.NotesSettingAsset)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
ranks := buildRankInfo(liveSetting)
owningIdList := []int{}
owningIDList := []int{}
err = ss.UserEng.Table("user_deck_unit").
Join("LEFT", "user_deck", "user_deck_unit.user_deck_id = user_deck.id").
Where("user_deck.user_id = ? AND user_deck.deck_id = ?", ss.UserID, playReq.UnitDeckID).
Cols("unit_owning_user_id").OrderBy("user_deck_unit.position ASC").Find(&owningIdList)
if ss.CheckErr(err) {
return
Cols("unit_owning_user_id").OrderBy("user_deck_unit.position ASC").Find(&owningIDList)
if err != nil {
return liveschema.PlayResp{}, err
}
if len(owningIdList) == 0 {
ss.CheckErr(errors.New("deck has no units"))
return
if len(owningIDList) == 0 {
return liveschema.PlayResp{}, errors.New("deck has no units")
}
museumBuff, err := getMuseumBuff(ss)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
myCenterUnitID, err := getDeckCenterUnitID(ss, playReq.UnitDeckID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
myLeaderSkill, err := getLeaderSkillData(ss, myCenterUnitID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
tomoUnitID, err := getSupportCenterUnitID(ss, int(playReq.PartyUserID), myCenterUnitID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
tomoLeaderSkill, err := getLeaderSkillData(ss, tomoUnitID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
memberTagCache := map[memberTagMatchKey]bool{}
unitDataMap, err := loadDeckUnitDataMap(ss, owningIdList)
if ss.CheckErr(err) {
return
unitDataMap, err := loadDeckUnitDataMap(ss, owningIDList)
if err != nil {
return liveschema.PlayResp{}, err
}
accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIdList)
if ss.CheckErr(err) {
return
accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIDList)
if err != nil {
return liveschema.PlayResp{}, err
}
removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIdList)
if ss.CheckErr(err) {
return
removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIDList)
if err != nil {
return liveschema.PlayResp{}, err
}
allRemovableSkillIDs := make([]int, 0)
for _, skillIDs := range removableSkillMap {
allRemovableSkillIDs = append(allRemovableSkillIDs, skillIDs...)
}
removableSkillMetaMap, err := loadRemovableSkillMetaMap(ss, allRemovableSkillIDs)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
unitList := make([]liveschema.UnitList, 0, len(owningIdList))
unitList := make([]liveschema.UnitList, 0, len(owningIDList))
var totalSmile, totalPure, totalCool float64
var totalHp int
for _, owningId := range owningIdList {
var totalHP int
for _, owningID := range owningIDList {
// 卡片基础属性
var baseSmile, basePure, baseCool, smileMax, pureMax, coolMax float64
unitData, ok := unitDataMap[owningId]
unitData, ok := unitDataMap[owningID]
if !ok {
ss.CheckErr(errors.New("unit data not found in deck"))
return
return liveschema.PlayResp{}, errors.New("unit data not found in deck")
}
baseSmile = float64(unitData.Smile)
basePure = float64(unitData.Cute)
@@ -120,7 +131,7 @@ func play(ctx *gin.Context) {
// fmt.Println("基础属性:", baseSmile, basePure, baseCool)
// 饰品属性加成(满级)
if bonus, ok := accessoryBonusMap[owningId]; ok {
if bonus, ok := accessoryBonusMap[owningID]; ok {
// 饰品属性加成(该加成会影响个宝等百分比宝石属性加成的计算,故先计算。)
baseSmile += bonus.Smile
basePure += bonus.Pure
@@ -153,14 +164,12 @@ func play(ctx *gin.Context) {
var skillSmile, skillPure, skillCool float64
// 宝石加成(满级)
removableSkillIds := removableSkillMap[owningId]
for _, sk := range removableSkillIds {
removableSkillIDs := removableSkillMap[owningID]
for _, skillID := range removableSkillIDs {
// 判断宝石效果类型(效果范围、效果类型、效果值、是否固定数值)
meta, ok := removableSkillMetaMap[sk]
meta, ok := removableSkillMetaMap[skillID]
if !ok {
ss.CheckErr(errors.New("removable skill meta not found"))
return
return liveschema.PlayResp{}, errors.New("removable skill meta not found")
}
if meta.FixedValueFlag == 1 {
@@ -174,37 +183,28 @@ func play(ctx *gin.Context) {
kissCool += meta.EffectValue
}
// fmt.Println("吻、眼神属性加成:", kissSmile, kissPure, kissCool)
} else {
// 仅效果类型为1、2、3的有属性加成
if meta.EffectType == 1 || meta.EffectType == 2 || meta.EffectType == 3 {
// 加成范围:2:全员 1:非全员
if meta.EffectRange == 2 {
switch meta.EffectType {
case 1:
skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100))
case 2:
skillPure += math.Ceil(basePure * (meta.EffectValue / 100))
case 3:
skillCool += math.Ceil(baseCool * (meta.EffectValue / 100))
}
// fmt.Println("全员类宝石属性加成:", skillSmile, skillPure, skillCool)
} else {
// refType: 1 -> 年级类加成, target_type -> 指定年级(这里不需要使用,因为能装上宝石肯定是符合的)
// refType: 2 -> 个宝
// refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石
if meta.TargetReferenceType == 1 || meta.TargetReferenceType == 2 { // 年级类和个宝都是百分比加成
switch meta.EffectType {
case 1:
skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100))
case 2:
skillPure += math.Ceil(basePure * (meta.EffectValue / 100))
case 3:
skillCool += math.Ceil(baseCool * (meta.EffectValue / 100))
}
// fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool)
}
}
continue
}
// 仅效果类型为1、2、3的有属性加成
if meta.EffectType != 1 && meta.EffectType != 2 && meta.EffectType != 3 {
continue
}
// 加成范围:2:全员 1:非全员
if meta.EffectRange == 2 || meta.TargetReferenceType == 1 || meta.TargetReferenceType == 2 {
// refType: 1 -> 年级类加成, target_type -> 指定年级(这里不需要使用,因为能装上宝石肯定是符合的)
// refType: 2 -> 个宝
// refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石
switch meta.EffectType {
case 1:
skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100))
case 2:
skillPure += math.Ceil(basePure * (meta.EffectValue / 100))
case 3:
skillCool += math.Ceil(baseCool * (meta.EffectValue / 100))
}
// fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool)
}
}
@@ -223,8 +223,8 @@ func play(ctx *gin.Context) {
// 主唱技能加成:副C技能
matched, err := matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, myLeaderSkill.MemberTagID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
var mySubSmile, mySubPure, mySubCool float64
@@ -247,9 +247,10 @@ func play(ctx *gin.Context) {
// 好友主唱技能加成:副C技能
matched, err = matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, tomoLeaderSkill.MemberTagID)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.PlayResp{}, err
}
var tomoSubSmile, tomoSubPure, tomoSubCool float64
if matched {
tomoSubSmile, tomoSubPure, tomoSubCool = applyAttributeBonus(
@@ -264,7 +265,7 @@ func play(ctx *gin.Context) {
totalSmile += smileMax + myCenterSmile + mySubSmile + tomoCenterSmile + tomoSubSmile
totalPure += pureMax + myCenterPure + mySubPure + tomoCenterPure + tomoSubPure
totalCool += coolMax + myCenterCool + mySubCool + tomoCenterCool + tomoSubCool
totalHp += unitData.MaxHp
totalHP += unitData.MaxHp
// fmt.Println("smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, totalSmile",
// smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile,
// smileMax+myCenterSmile+mySubSmile+tomoCenterSmile+tomoSubSmile)
@@ -276,45 +277,38 @@ func play(ctx *gin.Context) {
// coolMax+myCenterCool+mySubCool+tomoCenterCool+tomoSubCool)
// 单卡属性计算结果取上取整
fixedSmileMax := int(smileMax)
fixedPureMax := int(pureMax)
fixedCoolMax := int(coolMax)
// fmt.Println("单卡属性:", fixedSmileMax, fixedPureMax, fixedCoolMax)
unitList = append(unitList, liveschema.UnitList{
Smile: fixedSmileMax,
Cute: fixedPureMax,
Cool: fixedCoolMax,
Smile: int(smileMax),
Cute: int(pureMax),
Cool: int(coolMax),
})
// fmt.Println("单卡属性:", int(smileMax), int(pureMax), int(coolMax))
}
// 全部卡属性计算结果取上取整
fixedTotalSmile := int(math.Ceil(totalSmile))
fixedTotalPure := int(math.Ceil(totalPure))
fixedTotalCool := int(math.Ceil(totalCool))
// fmt.Println("全卡组属性:", fixedTotalSmile, fixedTotalPure, fixedTotalCool)
lives := []liveschema.PlayLiveList{}
lives = append(lives, liveschema.PlayLiveList{
LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID,
IsRandom: false,
AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag,
NotesList: notes,
lives := []liveschema.PlayLiveList{
{
LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID,
IsRandom: isRandom,
AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag,
NotesList: notes,
},
DeckInfo: liveschema.DeckInfo{
UnitDeckID: playReq.UnitDeckID,
TotalSmile: int(math.Ceil(totalSmile)),
TotalCute: int(math.Ceil(totalPure)),
TotalCool: int(math.Ceil(totalCool)),
TotalHp: totalHP,
PreparedHpDamage: 0,
UnitList: unitList,
},
},
DeckInfo: liveschema.DeckInfo{
UnitDeckID: playReq.UnitDeckID,
TotalSmile: fixedTotalSmile,
TotalCute: fixedTotalPure,
TotalCool: fixedTotalCool,
TotalHp: totalHp,
PreparedHpDamage: 0,
UnitList: unitList,
},
})
}
// fmt.Println("全卡组属性:", int(math.Ceil(totalSmile)), int(math.Ceil(totalPure)), int(math.Ceil(totalCool)))
playResp := liveschema.PlayResp{
return liveschema.PlayResp{
ResponseData: liveschema.PlayData{
RankInfo: ranks,
EnergyFullTime: "2023-03-20 01:28:55",
@@ -329,9 +323,7 @@ func play(ctx *gin.Context) {
},
ReleaseInfo: []any{},
StatusCode: 200,
}
ss.Respond(playResp)
}, nil
}
func init() {
+11 -4
View File
@@ -10,7 +10,6 @@ import (
liverecordschema "honoka-chan/internal/schema/liverecord"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -27,7 +26,7 @@ func preciseScore(ctx *gin.Context) {
}
// fmt.Println(ctx.MustGet("request_data").(string))
difficultyID, _ := strconv.Atoi(playScoreReq.LiveDifficultyID)
difficultyID := int(playScoreReq.LiveDifficultyID)
liveSetting, err := loadLiveSetting(ss, difficultyID)
if ss.CheckErr(err) {
@@ -43,6 +42,14 @@ func preciseScore(ctx *gin.Context) {
// 检查是否正在进行 Live
progress, _ := ss.GetLiveInProgress()
isRandom := false
if progress {
hasRandomLive, _, err := ss.GetActiveRandomLiveByDifficulty(difficultyID)
if ss.CheckErr(err) {
return
}
isRandom = hasRandomLive
}
// 检查是否有 Live 记录
liveRecord := ss.GetUserLiveRecord(difficultyID)
@@ -60,7 +67,7 @@ func preciseScore(ctx *gin.Context) {
HasRecord: false,
LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID,
IsRandom: false,
IsRandom: isRandom,
AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag,
NotesList: notesList,
@@ -70,7 +77,7 @@ func preciseScore(ctx *gin.Context) {
HasRecord: false,
LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID,
IsRandom: false,
IsRandom: isRandom,
AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag,
NotesList: notesList,
+31 -12
View File
@@ -1,6 +1,7 @@
package live
import (
"errors"
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
@@ -16,7 +17,9 @@ import (
func reward(ctx *gin.Context) {
ss := session.Get(ctx)
defer func() {
ss.ClearLiveInProgress()
if ss.UserEng != nil {
ss.ClearLiveInProgress()
}
ss.Finalize()
}()
@@ -26,37 +29,53 @@ func reward(ctx *gin.Context) {
return
}
playResp, err := BuildRewardResp(ss, playRewardReq, false)
if ss.CheckErr(err) {
return
}
ss.Respond(playResp)
}
func BuildRewardResp(ss *session.Session, playRewardReq liveschema.RewardReq, isRandom bool) (liveschema.RewardResp, error) {
difficultyID := playRewardReq.LiveDifficultyID
_, liveInfo := ss.GetLiveInfo(difficultyID)
if liveInfo == nil {
return liveschema.RewardResp{}, errors.New("live info not found")
}
liveInfo.IsRandom = isRandom
_, progress := ss.GetLiveInProgress()
if progress == nil {
return liveschema.RewardResp{}, errors.New("live progress not found")
}
_, deckInfo := ss.GetDeckInfo(progress.DeckID)
deckInfo.LiveDifficultyID = difficultyID
unitsList := []liveschema.PlayRewardUnitList{}
err = ss.UserEng.Table("user_deck_unit").
err := ss.UserEng.Table("user_deck_unit").
Join("LEFT", "user_deck", "user_deck_unit.user_deck_id = user_deck.id").
Where("user_deck.user_id = ? AND user_deck.deck_id = ?", ss.UserID, progress.DeckID).
Find(&unitsList)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.RewardResp{}, err
}
totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute
beforeLiveStatus, afterLiveStatus, err := ss.UpdateUserLiveStatus(difficultyID, totalScore, playRewardReq.MaxCombo)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.RewardResp{}, err
}
newGoalRewardIDs, err := ss.SyncUserLiveGoals(difficultyID, afterLiveStatus.HiScore, afterLiveStatus.HiComboCount, afterLiveStatus.ClearCnt)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.RewardResp{}, err
}
goalAccomplishedIDs := make([]any, 0, len(newGoalRewardIDs))
goalRewards := make([]any, 0, len(newGoalRewardIDs))
if len(newGoalRewardIDs) > 0 {
goalRewardRows, err := ss.GetLiveGoalRewardRowsByIDs(newGoalRewardIDs)
if ss.CheckErr(err) {
return
if err != nil {
return liveschema.RewardResp{}, err
}
for _, goalRewardID := range newGoalRewardIDs {
goalAccomplishedIDs = append(goalAccomplishedIDs, goalRewardID)
@@ -77,7 +96,7 @@ func reward(ctx *gin.Context) {
LiveInfo: []liveschema.RewardLiveInfo{
{
LiveDifficultyID: difficultyID,
IsRandom: false,
IsRandom: isRandom,
AcFlag: liveInfo.AcFlag,
SwingFlag: liveInfo.SwingFlag,
},
@@ -205,7 +224,7 @@ func reward(ctx *gin.Context) {
ss.UpdateUserLiveRecord(&newLiveRecord, liveSetting.PreciseScoreUpdateType)
}
ss.Respond(playResp)
return playResp, nil
}
func init() {
+39
View File
@@ -0,0 +1,39 @@
package rlive
import (
"errors"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/session"
)
func getRandomLiveByToken(ss *session.Session, token string) (usermodel.UserLiveRandom, error) {
if token == "" {
return usermodel.UserLiveRandom{}, errors.New("random live token is required")
}
row := usermodel.UserLiveRandom{}
has, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)).
Where("user_id = ? AND token = ?", ss.UserID, token).
Get(&row)
if err != nil {
return usermodel.UserLiveRandom{}, err
}
if !has {
return usermodel.UserLiveRandom{}, errors.New("random live session not found")
}
return row, nil
}
func deleteRandomLiveByToken(ss *session.Session, token string) {
if token == "" || ss.UserEng == nil {
return
}
_, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)).
Where("user_id = ? AND token = ?", ss.UserID, token).
Delete()
if ss.CheckErr(err) {
return
}
}
+41
View File
@@ -0,0 +1,41 @@
package rlive
import (
"errors"
livehandler "honoka-chan/internal/handler/live"
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
rliveschema "honoka-chan/internal/schema/rlive"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func continuee(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
req := rliveschema.TokenReq{}
err := honokautils.ParseRequestData(ctx, &req)
if ss.CheckErr(err) {
return
}
_, err = getRandomLiveByToken(ss, req.Token)
if ss.CheckErr(err) {
return
}
hasProgress, _ := ss.GetLiveInProgress()
if !hasProgress {
ss.CheckErr(errors.New("live progress not found"))
return
}
ss.Respond(livehandler.BuildContinueResp(ss))
}
func init() {
router.AddHandler("main.php", "POST", "/rlive/continue", middleware.Common, continuee)
}
+42
View File
@@ -0,0 +1,42 @@
package rlive
import (
livehandler "honoka-chan/internal/handler/live"
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
rliveschema "honoka-chan/internal/schema/rlive"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func gameOver(ctx *gin.Context) {
ss := session.Get(ctx)
token := ""
defer func() {
if ss.UserEng != nil {
deleteRandomLiveByToken(ss, token)
ss.ClearLiveInProgress()
}
ss.Finalize()
}()
req := rliveschema.TokenReq{}
err := honokautils.ParseRequestData(ctx, &req)
if ss.CheckErr(err) {
return
}
token = req.Token
_, err = getRandomLiveByToken(ss, req.Token)
if ss.CheckErr(err) {
return
}
ss.Respond(livehandler.BuildGameOverResp())
}
func init() {
router.AddHandler("main.php", "POST", "/rlive/gameover", middleware.Common, gameOver)
}
+251
View File
@@ -0,0 +1,251 @@
package rlive
import (
"crypto/rand"
"errors"
livehandler "honoka-chan/internal/handler/live"
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
rliveschema "honoka-chan/internal/schema/rlive"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
pkgutils "honoka-chan/pkg/utils"
"math/big"
"os"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
)
var rliveCST = time.FixedZone("CST", 8*3600)
type randomLiveCandidateRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"`
NotesSettingAsset string `xorm:"notes_setting_asset"`
AcFlag int `xorm:"ac_flag"`
SwingFlag int `xorm:"swing_flag"`
}
func lot(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
req := rliveschema.LotReq{}
err := honokautils.ParseRequestData(ctx, &req)
if ss.CheckErr(err) {
return
}
if err := validateLotReq(req, time.Now()); ss.CheckErr(err) {
return
}
randomLive, err := loadOrCreateRandomLive(ss, req)
if ss.CheckErr(err) {
return
}
partyListData, err := livehandler.BuildPartyListData(ss)
if ss.CheckErr(err) {
return
}
ss.Respond(rliveschema.LotResp{
ResponseData: rliveschema.LotData{
LiveInfo: rliveschema.LiveInfo{
LiveDifficultyID: randomLive.LiveDifficultyID,
IsRandom: true,
AcFlag: randomLive.AcFlag,
SwingFlag: randomLive.SwingFlag,
},
HasSlideNotes: randomLive.SwingFlag,
PartyList: partyListData.PartyList,
TrainingEnergy: partyListData.TrainingEnergy,
TrainingEnergyMax: partyListData.TrainingEnergyMax,
Token: randomLive.Token,
},
ReleaseInfo: []any{},
StatusCode: 200,
})
}
type randomLiveResult struct {
Token string
LiveDifficultyID int
AcFlag int
SwingFlag int
}
func loadOrCreateRandomLive(ss *session.Session, req rliveschema.LotReq) (randomLiveResult, error) {
sessionRow := usermodel.UserLiveRandom{}
has, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)).
Where("user_id = ?", ss.UserID).
Where("attribute = ?", req.Attribute).
Where("difficulty = ?", req.Difficulty).
Where("member_category = ?", req.MemberCategory).
Get(&sessionRow)
if err != nil {
return randomLiveResult{}, err
}
if has {
liveInfo, err := loadRandomLiveInfo(ss, sessionRow.LiveDifficultyID)
if err != nil {
return randomLiveResult{}, err
}
return randomLiveResult{
Token: sessionRow.Token,
LiveDifficultyID: sessionRow.LiveDifficultyID,
AcFlag: liveInfo.AcFlag,
SwingFlag: liveInfo.SwingFlag,
}, nil
}
candidates, err := listRandomLiveCandidates(ss, req.Difficulty, req.Attribute, req.MemberCategory)
if err != nil {
return randomLiveResult{}, err
}
if len(candidates) == 0 {
return randomLiveResult{}, errors.New("no random live candidates available")
}
index, err := cryptoRandIndex(len(candidates))
if err != nil {
return randomLiveResult{}, err
}
picked := candidates[index]
token := pkgutils.RandomStr(64)
if token == "" {
return randomLiveResult{}, errors.New("failed to generate random live token")
}
_, err = ss.UserEng.Insert(&usermodel.UserLiveRandom{
UserID: ss.UserID,
Attribute: req.Attribute,
Difficulty: req.Difficulty,
MemberCategory: req.MemberCategory,
Token: token,
LiveDifficultyID: picked.LiveDifficultyID,
InProgress: false,
})
if err != nil {
return randomLiveResult{}, err
}
return randomLiveResult{
Token: token,
LiveDifficultyID: picked.LiveDifficultyID,
AcFlag: picked.AcFlag,
SwingFlag: picked.SwingFlag,
}, nil
}
func validateLotReq(req rliveschema.LotReq, now time.Time) error {
if req.Difficulty < 1 || req.Difficulty > 4 {
return errors.New("invalid difficulty")
}
if req.Attribute < 1 || req.Attribute > 3 {
return errors.New("invalid attribute")
}
if req.MemberCategory < 0 {
return errors.New("invalid member category")
}
if int(now.In(rliveCST).Weekday())%3+1 != req.Attribute {
return errors.New("attribute does not match current random live rotation")
}
return nil
}
func listRandomLiveCandidates(ss *session.Session, difficulty, attribute, memberCategory int) ([]randomLiveCandidateRow, error) {
rows := []randomLiveCandidateRow{}
sql := `
SELECT
difficulty.live_difficulty_id,
setting.notes_setting_asset,
setting.ac_flag,
setting.swing_flag
FROM live_setting_m AS setting
INNER JOIN (
SELECT live_setting_id, live_difficulty_id FROM special_live_m
UNION
SELECT live_setting_id, live_difficulty_id FROM normal_live_m
) AS difficulty ON setting.live_setting_id = difficulty.live_setting_id
INNER JOIN live_track_m AS track ON track.live_track_id = setting.live_track_id
WHERE setting.difficulty = ? AND setting.attribute_icon_id = ?
`
args := []any{difficulty, attribute}
if memberCategory > 0 {
sql += ` AND track.member_category = ?`
args = append(args, memberCategory)
}
sql += `
ORDER BY difficulty.live_difficulty_id ASC
`
err := ss.MainEng.SQL(sql, args...).Find(&rows)
if err != nil {
return nil, err
}
result := make([]randomLiveCandidateRow, 0, len(rows))
for _, row := range rows {
if !hasRandomLiveBeatmap(row.NotesSettingAsset) {
continue
}
result = append(result, row)
}
return result, nil
}
func loadRandomLiveInfo(ss *session.Session, liveDifficultyID int) (randomLiveCandidateRow, error) {
rows := []randomLiveCandidateRow{}
err := ss.MainEng.SQL(`
SELECT
difficulty.live_difficulty_id,
setting.notes_setting_asset,
setting.ac_flag,
setting.swing_flag
FROM live_setting_m AS setting
INNER JOIN (
SELECT live_setting_id, live_difficulty_id FROM special_live_m
UNION
SELECT live_setting_id, live_difficulty_id FROM normal_live_m
) AS difficulty ON setting.live_setting_id = difficulty.live_setting_id
WHERE difficulty.live_difficulty_id = ?
LIMIT 1
`, liveDifficultyID).Find(&rows)
if err != nil {
return randomLiveCandidateRow{}, err
}
if len(rows) == 0 {
return randomLiveCandidateRow{}, errors.New("random live difficulty not found")
}
return rows[0], nil
}
func hasRandomLiveBeatmap(notesSettingAsset string) bool {
if notesSettingAsset == "" {
return false
}
path := filepath.Join("assets/serverdata/beatmaps", notesSettingAsset)
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func cryptoRandIndex(length int) (int, error) {
if length <= 0 {
return 0, errors.New("invalid random candidate length")
}
n, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
if err != nil {
return 0, err
}
return int(n.Int64()), nil
}
func init() {
router.AddHandler("main.php", "POST", "/rlive/lot", middleware.Common, lot)
}
+55
View File
@@ -0,0 +1,55 @@
package rlive
import (
livehandler "honoka-chan/internal/handler/live"
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
rliveschema "honoka-chan/internal/schema/rlive"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func play(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
req := rliveschema.PlayReq{}
err := honokautils.ParseRequestData(ctx, &req)
if ss.CheckErr(err) {
return
}
randomLive, err := getRandomLiveByToken(ss, req.Token)
if ss.CheckErr(err) {
return
}
if err := ss.ResetRandomLiveInProgress(); ss.CheckErr(err) {
return
}
ss.ClearLiveInProgress()
ss.RegisterLiveInProgress(req.UnitDeckID)
_, err = ss.UserEng.Table(new(usermodel.UserLiveRandom)).
Where("user_id = ? AND token = ?", ss.UserID, req.Token).
Cols("in_progress").
Update(&usermodel.UserLiveRandom{InProgress: true})
if ss.CheckErr(err) {
return
}
playResp, err := livehandler.BuildPlayResp(ss, req.ToLivePlayReq(randomLive.LiveDifficultyID), true)
if ss.CheckErr(err) {
return
}
ss.Respond(playResp)
}
func init() {
router.AddHandler("main.php", "POST", "/rlive/play", middleware.Common, play)
}
+59
View File
@@ -0,0 +1,59 @@
package rlive
import (
"errors"
livehandler "honoka-chan/internal/handler/live"
"honoka-chan/internal/middleware"
"honoka-chan/internal/router"
rliveschema "honoka-chan/internal/schema/rlive"
"honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils"
"github.com/gin-gonic/gin"
)
func reward(ctx *gin.Context) {
ss := session.Get(ctx)
token := ""
defer func() {
if ss.UserEng != nil {
deleteRandomLiveByToken(ss, token)
ss.ClearLiveInProgress()
}
ss.Finalize()
}()
req := rliveschema.RewardReq{}
err := honokautils.ParseRequestData(ctx, &req)
if ss.CheckErr(err) {
return
}
token = req.Token
randomLive, err := getRandomLiveByToken(ss, req.Token)
if ss.CheckErr(err) {
return
}
hasProgress, _ := ss.GetLiveInProgress()
if !hasProgress {
ss.CheckErr(errors.New("live progress not found"))
return
}
if req.LiveDifficultyID != 0 && req.LiveDifficultyID != randomLive.LiveDifficultyID {
ss.CheckErr(errors.New("random live difficulty mismatch"))
return
}
rewardResp, err := livehandler.BuildRewardResp(ss, req.ToLiveRewardReq(randomLive.LiveDifficultyID), true)
if ss.CheckErr(err) {
return
}
ss.Respond(rewardResp)
}
func init() {
router.AddHandler("main.php", "POST", "/rlive/reward", middleware.Common, reward)
}