245 lines
6.7 KiB
Go
245 lines
6.7 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
|
|
NamePinyin string
|
|
Image string
|
|
Audio string
|
|
Description string
|
|
}
|
|
|
|
// Option 分支选项 VO。
|
|
type Option struct {
|
|
OptionId int64
|
|
Text string
|
|
TextPinyin string
|
|
FeedbackPros string
|
|
FeedbackCons string
|
|
Audio string
|
|
Prop *Element
|
|
}
|
|
|
|
// Node 关卡节点 VO(决策/终局)。
|
|
type Node struct {
|
|
NodeId int64
|
|
Title string
|
|
Content string
|
|
ContentPinyin string
|
|
Image string
|
|
Audio string
|
|
Character *Element
|
|
InteractionType int
|
|
Config string
|
|
Script string
|
|
NodeType int
|
|
ResultType int
|
|
Options []*Option
|
|
}
|
|
|
|
// LevelDetail 关卡详情:场景 + 入口节点决策树 + 用户进度。
|
|
type LevelDetail struct {
|
|
LevelId int64
|
|
Title string
|
|
Scene *Element
|
|
SceneContent string
|
|
SceneContentPinyin 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(),
|
|
SceneContentPinyin: levelRec["scene_content_pinyin"].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(),
|
|
NamePinyin: r["name_pinyin"].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(),
|
|
ContentPinyin: nodeRec["content_pinyin"].String(),
|
|
Image: nodeRec["image"].String(),
|
|
Audio: nodeRec["audio"].String(),
|
|
Character: elements[nodeRec["character_id"].Int64()],
|
|
InteractionType: nodeRec["interaction_type"].Int(),
|
|
Config: nodeRec["config"].String(),
|
|
Script: nodeRec["script"].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(),
|
|
TextPinyin: o["text_pinyin"].String(),
|
|
FeedbackPros: o["feedback_pros"].String(),
|
|
FeedbackCons: o["feedback_cons"].String(),
|
|
Audio: o["audio"].String(),
|
|
Prop: elements[o["prop_id"].Int64()],
|
|
})
|
|
}
|
|
return node
|
|
}
|