Files
36Wisdom/biz/service/level.go
T
adminandClaude Opus 4.7 414c7de007 feat: 分支闯关判分与终局结算(含单测)
choose 校验节点/选项归属与解锁链,终局在 common.WithLock 内按
技术设计 4.3/4.4 结算:首次到达才结算、连续失败限扣、余额不为负、
完美集齐全终局 +20 并解锁计策卡与下一计;路径流水尽力而为。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-13 12:47:01 +08:00

229 lines
6.0 KiB
Go

package service
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"36wisdom/biz/consts"
"36wisdom/biz/dao"
)
type level struct{}
var Level = &level{}
// 成长等级:按完美关卡数映射称号(技术设计.md 4.10,派生值不落库)。
var levelTitles = []struct {
MinPerfect int
Title string
}{
{MinPerfect: 0, Title: "小学徒"},
{MinPerfect: 5, Title: "小书童"},
{MinPerfect: 10, Title: "小军师"},
{MinPerfect: 20, Title: "军师"},
{MinPerfect: 30, Title: "大将军"},
}
// LevelOf 按完美关卡数返回等级(1-5)与称号。
func LevelOf(perfectCount int) (level int, title string) {
level = 1
title = levelTitles[0].Title
for i, lt := range levelTitles {
if perfectCount >= lt.MinPerfect {
level = i + 1
title = lt.Title
}
}
return
}
// Element 元素 VO(场景/人物/道具)。
type Element struct {
Id int64
EType int
Name string
Image string
Audio string
Description string
}
// Option 分支选项 VO。
type Option struct {
OptionId int64
Text string
Prop *Element
}
// Node 关卡节点 VO(决策/终局)。
type Node struct {
NodeId int64
Title string
Content string
Image string
Audio string
Character *Element
InteractionType int
Config string
NodeType int
ResultType int
Options []*Option
}
// LevelDetail 关卡详情:场景 + 入口节点决策树 + 用户进度。
type LevelDetail struct {
LevelId int64
Title string
Scene *Element
SceneContent string
SceneImage string
SceneAudio string
Entry *Node
TotalFinals int
Perfect bool
Stars int
}
// Detail 关卡详情:节点/选项/元素分表取回,内存组装决策树(入口节点)。
func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (*LevelDetail, error) {
child, err := getChildOf(ctx, parentUid, childId)
if err != nil {
return nil, err
}
levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One()
if err != nil {
return nil, err
}
if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled {
return nil, gerror.New("关卡不存在")
}
if levelRec["age_group"].String() != child["age_group"].String() {
return nil, gerror.New("关卡不属于当前年龄段")
}
nodeRecs, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
Where("level_id", levelId).Where("status", consts.StatusEnabled).
Order("sort_order ASC").All()
if err != nil {
return nil, err
}
nodeIds := make([]int64, 0, len(nodeRecs))
for _, n := range nodeRecs {
nodeIds = append(nodeIds, n["id"].Int64())
}
optionRecs := []gdb.Record{}
if len(nodeIds) > 0 {
optionRecs, err = dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
WhereIn("node_id", nodeIds).Where("status", consts.StatusEnabled).
Order("sort_order ASC").All()
if err != nil {
return nil, err
}
}
elements, err := elementsOf(ctx, levelRec, nodeRecs, optionRecs)
if err != nil {
return nil, err
}
progressRec, err := dao.UserProgress.Model().Ctx(ctx).
Where("child_id", childId).Where("level_id", levelId).One()
if err != nil {
return nil, err
}
var entryRec gdb.Record
totalFinals := 0
for _, n := range nodeRecs {
if n["is_entry"].Int() == 1 {
entryRec = n
}
if n["result_type"].Int() > 0 {
totalFinals++
}
}
if entryRec.IsEmpty() {
return nil, gerror.New("关卡入口缺失")
}
return &LevelDetail{
LevelId: levelRec["id"].Int64(),
Title: levelRec["title"].String(),
Scene: elements[levelRec["scene_id"].Int64()],
SceneContent: levelRec["scene_content"].String(),
SceneImage: levelRec["scene_image"].String(),
SceneAudio: levelRec["scene_audio"].String(),
Entry: buildNode(entryRec, optionRecs, elements),
TotalFinals: totalFinals,
Perfect: progressRec["perfect"].Int() == 1,
Stars: progressRec["stars"].Int(),
}, nil
}
// elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。
func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*Element, error) {
ids := []int64{levelRec["scene_id"].Int64()}
for _, n := range nodeRecs {
if cid := n["character_id"].Int64(); cid > 0 {
ids = append(ids, cid)
}
}
for _, o := range optionRecs {
if pid := o["prop_id"].Int64(); pid > 0 {
ids = append(ids, pid)
}
}
ids = uniqueInt64(ids)
m := make(map[int64]*Element, len(ids))
if len(ids) == 0 {
return m, nil
}
recs, err := dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)).
WhereIn("id", ids).Where("status", consts.StatusEnabled).All()
if err != nil {
return nil, err
}
for _, r := range recs {
m[r["id"].Int64()] = &Element{
Id: r["id"].Int64(),
EType: r["e_type"].Int(),
Name: r["name"].String(),
Image: r["image"].String(),
Audio: r["audio"].String(),
Description: r["description"].String(),
}
}
return m, nil
}
// buildNode 组装单个节点的选项与人物/道具元素。
func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*Element) *Node {
node := &Node{
NodeId: nodeRec["id"].Int64(),
Title: nodeRec["title"].String(),
Content: nodeRec["content"].String(),
Image: nodeRec["image"].String(),
Audio: nodeRec["audio"].String(),
Character: elements[nodeRec["character_id"].Int64()],
InteractionType: nodeRec["interaction_type"].Int(),
Config: nodeRec["config"].String(),
NodeType: nodeRec["node_type"].Int(),
ResultType: nodeRec["result_type"].Int(),
}
for _, o := range optionRecs {
if o["node_id"].Int64() != node.NodeId {
continue
}
node.Options = append(node.Options, &Option{
OptionId: o["id"].Int64(),
Text: o["text"].String(),
Prop: elements[o["prop_id"].Int64()],
})
}
return node
}