mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Proto generation: outputs to gen/proto/ (module=lunar-tear/server).
|
||||
# All proto files have go_package. Only protos used by the server are generated here
|
||||
# (generating all would put them in one package and cause name clashes).
|
||||
PROTO_USED = proto/banner.proto proto/battle.proto proto/bighunt.proto proto/cageornament.proto proto/character.proto proto/characterboard.proto proto/characterviewer.proto proto/companion.proto proto/config.proto proto/contentsstory.proto proto/costume.proto proto/data.proto proto/deck.proto proto/dokan.proto proto/explore.proto proto/friend.proto proto/gacha.proto proto/gameplay.proto proto/gift.proto proto/gimmick.proto proto/labyrinth.proto proto/loginbonus.proto proto/material.proto proto/mission.proto proto/movie.proto proto/navicutin.proto proto/omikuji.proto proto/notification.proto proto/parts.proto proto/portalcage.proto proto/pvp.proto proto/quest.proto proto/reward.proto proto/shop.proto proto/sidestoryquest.proto proto/tutorial.proto proto/user.proto proto/weapon.proto
|
||||
|
||||
proto:
|
||||
protoc -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server
|
||||
@echo "Generated in gen/proto/"
|
||||
|
||||
.PHONY: proto
|
||||
@@ -0,0 +1,205 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gacha"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/service"
|
||||
"lunar-tear/server/internal/store/memory"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type loggingListener struct {
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (l loggingListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
log.Printf("[gRPC] Accept error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[gRPC] New connection from %v", conn.RemoteAddr())
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func startGRPC(
|
||||
host string,
|
||||
octoURL string,
|
||||
userStore *memory.MemoryStore,
|
||||
questEngine *questflow.QuestHandler,
|
||||
gachaHandler *gacha.GachaHandler,
|
||||
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
|
||||
loginBonusCatalog *masterdata.LoginBonusCatalog,
|
||||
characterViewerCatalog *masterdata.CharacterViewerCatalog,
|
||||
shopCatalog *masterdata.ShopCatalog,
|
||||
costumeCatalog *masterdata.CostumeCatalog,
|
||||
omikujiCatalog *masterdata.OmikujiCatalog,
|
||||
weaponCatalog *masterdata.WeaponCatalog,
|
||||
exploreCatalog *masterdata.ExploreCatalog,
|
||||
gimmickCatalog *masterdata.GimmickCatalog,
|
||||
characterBoardCatalog *masterdata.CharacterBoardCatalog,
|
||||
partsCatalog *masterdata.PartsCatalog,
|
||||
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
|
||||
companionCatalog *masterdata.CompanionCatalog,
|
||||
materialCatalog *masterdata.MaterialCatalog,
|
||||
gameConfig *masterdata.GameConfig,
|
||||
sideStoryCatalog *masterdata.SideStoryCatalog,
|
||||
bigHuntCatalog *masterdata.BigHuntCatalog,
|
||||
) {
|
||||
lis, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen on :443: %v", err)
|
||||
}
|
||||
lis = loggingListener{Listener: lis}
|
||||
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(loggingInterceptor, timeSyncInterceptor),
|
||||
grpc.UnknownServiceHandler(loggingUnknownService),
|
||||
)
|
||||
|
||||
registerServices(grpcServer,
|
||||
host,
|
||||
octoURL,
|
||||
userStore,
|
||||
questEngine,
|
||||
gachaHandler,
|
||||
cageOrnamentCatalog,
|
||||
loginBonusCatalog,
|
||||
characterViewerCatalog,
|
||||
shopCatalog,
|
||||
costumeCatalog,
|
||||
omikujiCatalog,
|
||||
weaponCatalog,
|
||||
exploreCatalog,
|
||||
gimmickCatalog,
|
||||
characterBoardCatalog,
|
||||
partsCatalog,
|
||||
characterRebirthCatalog,
|
||||
companionCatalog,
|
||||
materialCatalog,
|
||||
gameConfig,
|
||||
sideStoryCatalog,
|
||||
bigHuntCatalog,
|
||||
)
|
||||
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
log.Printf("gRPC server listening on :443")
|
||||
log.Printf("client host address: %s:443", host)
|
||||
|
||||
if err := grpcServer.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func registerServices(
|
||||
srv *grpc.Server,
|
||||
host string,
|
||||
octoURL string,
|
||||
userStore *memory.MemoryStore,
|
||||
questEngine *questflow.QuestHandler,
|
||||
gachaHandler *gacha.GachaHandler,
|
||||
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
|
||||
loginBonusCatalog *masterdata.LoginBonusCatalog,
|
||||
characterViewerCatalog *masterdata.CharacterViewerCatalog,
|
||||
shopCatalog *masterdata.ShopCatalog,
|
||||
costumeCatalog *masterdata.CostumeCatalog,
|
||||
omikujiCatalog *masterdata.OmikujiCatalog,
|
||||
weaponCatalog *masterdata.WeaponCatalog,
|
||||
exploreCatalog *masterdata.ExploreCatalog,
|
||||
gimmickCatalog *masterdata.GimmickCatalog,
|
||||
characterBoardCatalog *masterdata.CharacterBoardCatalog,
|
||||
partsCatalog *masterdata.PartsCatalog,
|
||||
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
|
||||
companionCatalog *masterdata.CompanionCatalog,
|
||||
materialCatalog *masterdata.MaterialCatalog,
|
||||
gameConfig *masterdata.GameConfig,
|
||||
sideStoryCatalog *masterdata.SideStoryCatalog,
|
||||
bigHuntCatalog *masterdata.BigHuntCatalog,
|
||||
) {
|
||||
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(userStore))
|
||||
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore))
|
||||
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
|
||||
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(host, int32(443), octoURL))
|
||||
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
|
||||
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine))
|
||||
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, userStore, gachaHandler))
|
||||
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
|
||||
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
|
||||
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog))
|
||||
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine))
|
||||
pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore))
|
||||
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, cageOrnamentCatalog, questEngine.Granter))
|
||||
pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore))
|
||||
pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore))
|
||||
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, loginBonusCatalog))
|
||||
pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore))
|
||||
pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore))
|
||||
pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore))
|
||||
pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore))
|
||||
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, characterViewerCatalog))
|
||||
pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore))
|
||||
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter))
|
||||
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig))
|
||||
pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore))
|
||||
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog))
|
||||
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig))
|
||||
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog))
|
||||
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog))
|
||||
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig))
|
||||
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig))
|
||||
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig))
|
||||
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig))
|
||||
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog))
|
||||
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine))
|
||||
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter))
|
||||
}
|
||||
|
||||
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
log.Printf(">>> %s", info.FullMethod)
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
log.Printf("<<< %s ERROR: %v", info.FullMethod, err)
|
||||
} else {
|
||||
log.Printf("<<< %s OK", info.FullMethod)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func timeSyncInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
resp, err := handler(ctx, req)
|
||||
switch info.FullMethod {
|
||||
case "/apb.api.user.UserService/Auth",
|
||||
"/apb.api.user.UserService/RegisterUser",
|
||||
"/apb.api.user.UserService/TransferUser":
|
||||
default:
|
||||
grpc.SetTrailer(ctx, metadata.Pairs(
|
||||
"x-apb-response-datetime", fmt.Sprintf("%d", gametime.NowMillis()),
|
||||
))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func loggingUnknownService(_ any, stream grpc.ServerStream) error {
|
||||
fullMethod, ok := grpc.MethodFromServerStream(stream)
|
||||
if !ok {
|
||||
fullMethod = "<unknown>"
|
||||
}
|
||||
log.Printf(">>> %s", fullMethod)
|
||||
err := status.Errorf(codes.Unimplemented, "unknown service or method %s", fullMethod)
|
||||
log.Printf("<<< %s ERROR: %v", fullMethod, err)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"lunar-tear/server/internal/service"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func startHTTP(port int, resourcesBaseURL string) {
|
||||
octoServer := service.NewOctoHTTPServer(resourcesBaseURL)
|
||||
h2s := &http2.Server{}
|
||||
octoHandler := h2c.NewHandler(octoServer.Handler(), h2s)
|
||||
log.Printf("Octo HTTP server listening on :%d (HTTP/1.1 + h2c)", port)
|
||||
srv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: octoHandler}
|
||||
http2.ConfigureServer(srv, h2s)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatalf("HTTP server on %d failed: %v", port, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"lunar-tear/server/internal/gacha"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store/memory"
|
||||
)
|
||||
|
||||
func main() {
|
||||
httpPort := flag.Int("http-port", 8080, "HTTP server port (Octo API)")
|
||||
host := flag.String("host", "127.0.0.1", "hostname the client will connect to")
|
||||
scene := flag.Int("scene", 0, "Bootstrap to scene N (0 = fresh start)")
|
||||
starterItems := flag.Bool("starter-items", false, "Grant starter items to new users")
|
||||
flag.Parse()
|
||||
|
||||
octoURL := "http://" + *host + ":" + strconv.Itoa(*httpPort)
|
||||
prefix := octoURL + "/"
|
||||
padLen := 43 - len(prefix)
|
||||
resourcesBaseURL := ""
|
||||
if padLen < 1 {
|
||||
log.Printf("[config] host:port too long for 43-char resource URL; list.bin will be served unchanged")
|
||||
} else {
|
||||
resourcesBaseURL = prefix + strings.Repeat("r", padLen)
|
||||
}
|
||||
|
||||
go startHTTP(*httpPort, resourcesBaseURL)
|
||||
|
||||
snapshotDir := "snapshots"
|
||||
if err := os.MkdirAll(snapshotDir, 0755); err != nil {
|
||||
log.Fatalf("create snapshot dir: %v", err)
|
||||
}
|
||||
|
||||
gameConfig, err := masterdata.LoadGameConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("load game config: %v", err)
|
||||
}
|
||||
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
|
||||
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
|
||||
|
||||
partsCatalog, err := masterdata.LoadPartsCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load parts catalog: %v", err)
|
||||
}
|
||||
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
|
||||
|
||||
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
|
||||
if err != nil {
|
||||
log.Fatalf("load quest catalog: %v", err)
|
||||
}
|
||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
|
||||
userStore := memory.New(gametime.Now,
|
||||
memory.WithSnapshotDir(snapshotDir),
|
||||
memory.WithSceneId(int32(*scene)),
|
||||
memory.WithStarterItems(*starterItems),
|
||||
)
|
||||
if *scene != 0 {
|
||||
log.Printf("bootstrap scene: %d (from snapshot)", *scene)
|
||||
}
|
||||
|
||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load gacha catalog: %v", err)
|
||||
}
|
||||
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
|
||||
|
||||
gachaPool, err := masterdata.LoadGachaPool()
|
||||
if err != nil {
|
||||
log.Fatalf("load gacha pool: %v", err)
|
||||
}
|
||||
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
|
||||
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
|
||||
|
||||
shopCatalog, err := masterdata.LoadShopCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load shop catalog: %v", err)
|
||||
}
|
||||
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
|
||||
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
|
||||
|
||||
gachaPool.BuildShopFeatured(shopCatalog)
|
||||
gachaPool.PruneUnpairedCostumes()
|
||||
gachaPool.BuildFeaturedMapping(gachaEntries)
|
||||
gachaPool.BuildBannerPools(gachaEntries)
|
||||
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
|
||||
userStore.ReplaceCatalog(gachaEntries)
|
||||
|
||||
dupExchange, err := masterdata.LoadDupExchange()
|
||||
if err != nil {
|
||||
log.Fatalf("load dup exchange: %v", err)
|
||||
}
|
||||
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
|
||||
if err != nil {
|
||||
log.Fatalf("enrich dup exchange: %v", err)
|
||||
}
|
||||
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
|
||||
|
||||
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
|
||||
|
||||
conditionResolver, err := masterdata.LoadConditionResolver()
|
||||
if err != nil {
|
||||
log.Fatalf("load condition resolver: %v", err)
|
||||
}
|
||||
|
||||
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
|
||||
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
|
||||
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
|
||||
omikujiCatalog := masterdata.LoadOmikujiCatalog()
|
||||
|
||||
materialCatalog, err := masterdata.LoadMaterialCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load material catalog: %v", err)
|
||||
}
|
||||
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
|
||||
|
||||
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
|
||||
if err != nil {
|
||||
log.Fatalf("load costume catalog: %v", err)
|
||||
}
|
||||
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
|
||||
|
||||
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
|
||||
if err != nil {
|
||||
log.Fatalf("load weapon catalog: %v", err)
|
||||
}
|
||||
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
|
||||
|
||||
exploreCatalog, err := masterdata.LoadExploreCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load explore catalog: %v", err)
|
||||
}
|
||||
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
|
||||
|
||||
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
|
||||
if err != nil {
|
||||
log.Fatalf("load gimmick catalog: %v", err)
|
||||
}
|
||||
|
||||
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load character board catalog: %v", err)
|
||||
}
|
||||
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
||||
|
||||
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load character rebirth catalog: %v", err)
|
||||
}
|
||||
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
||||
|
||||
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
||||
if err != nil {
|
||||
log.Fatalf("load companion catalog: %v", err)
|
||||
}
|
||||
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
|
||||
|
||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
|
||||
|
||||
startGRPC(
|
||||
*host,
|
||||
octoURL,
|
||||
userStore,
|
||||
questHandler,
|
||||
gachaHandler,
|
||||
cageOrnamentCatalog,
|
||||
loginBonusCatalog,
|
||||
characterViewerCatalog,
|
||||
shopCatalog,
|
||||
costumeCatalog,
|
||||
omikujiCatalog,
|
||||
weaponCatalog,
|
||||
exploreCatalog,
|
||||
gimmickCatalog,
|
||||
characterBoardCatalog,
|
||||
partsCatalog,
|
||||
characterRebirthCatalog,
|
||||
companionCatalog,
|
||||
materialCatalog,
|
||||
gameConfig,
|
||||
sideStoryCatalog,
|
||||
bigHuntCatalog,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module lunar-tear/server
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
@@ -0,0 +1,223 @@
|
||||
package gacha
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
)
|
||||
|
||||
type RateTier struct {
|
||||
Weight int
|
||||
PossessionType int32
|
||||
RarityType model.RarityType
|
||||
}
|
||||
|
||||
type DrawnItem struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
RarityType model.RarityType
|
||||
CharacterId int32
|
||||
}
|
||||
|
||||
var premiumRates = []RateTier{
|
||||
{200, int32(model.PossessionTypeCostume), model.RaritySSRare},
|
||||
{300, int32(model.PossessionTypeWeapon), model.RaritySSRare},
|
||||
{500, int32(model.PossessionTypeCostume), model.RaritySRare},
|
||||
{1000, int32(model.PossessionTypeWeapon), model.RaritySRare},
|
||||
{8000, int32(model.PossessionTypeWeapon), model.RarityRare},
|
||||
}
|
||||
|
||||
func DrawPremium(bp *masterdata.BannerPool, count int, fixedRarityMin int32, fixedCount int, rateMultiplier float64) []DrawnItem {
|
||||
result := make([]DrawnItem, 0, count)
|
||||
rates := adjustRates(premiumRates, rateMultiplier)
|
||||
totalWeight := 0
|
||||
for _, r := range rates {
|
||||
totalWeight += r.Weight
|
||||
}
|
||||
|
||||
for i := range count {
|
||||
isGuaranteeSlot := fixedCount > 0 && i >= count-fixedCount
|
||||
item := rollOne(bp, rates, totalWeight)
|
||||
|
||||
if isGuaranteeSlot && item.RarityType < fixedRarityMin {
|
||||
item = rollAtMinRarity(bp, rates, fixedRarityMin)
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func DrawBox(items []BoxItem, count int) []DrawnItem {
|
||||
var available []int
|
||||
for i, item := range items {
|
||||
remaining := item.MaxCount - item.DrewCount
|
||||
for range remaining {
|
||||
available = append(available, i)
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]DrawnItem, 0, count)
|
||||
for i := 0; i < count && len(available) > 0; i++ {
|
||||
pick := rand.Intn(len(available))
|
||||
idx := available[pick]
|
||||
item := items[idx]
|
||||
result = append(result, DrawnItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
RarityType: item.RarityType,
|
||||
})
|
||||
items[idx].DrewCount++
|
||||
available = append(available[:pick], available[pick+1:]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func DrawReward(materials []masterdata.GachaPoolItem, count int) []DrawnItem {
|
||||
if len(materials) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]DrawnItem, 0, count)
|
||||
for range count {
|
||||
m := materials[rand.Intn(len(materials))]
|
||||
result = append(result, DrawnItem{
|
||||
PossessionType: m.PossessionType,
|
||||
PossessionId: m.PossessionId,
|
||||
RarityType: m.RarityType,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type BoxItem struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
RarityType model.RarityType
|
||||
Count int32
|
||||
MaxCount int32
|
||||
DrewCount int32
|
||||
IsTarget bool
|
||||
}
|
||||
|
||||
func adjustRates(base []RateTier, multiplier float64) []RateTier {
|
||||
if multiplier == 1.0 || multiplier == 0 {
|
||||
return base
|
||||
}
|
||||
adjusted := make([]RateTier, len(base))
|
||||
copy(adjusted, base)
|
||||
|
||||
var fourStarExtra int
|
||||
var nonFourStar int
|
||||
for i, r := range adjusted {
|
||||
if r.RarityType >= model.RaritySSRare {
|
||||
extra := int(float64(r.Weight) * (multiplier - 1.0))
|
||||
adjusted[i].Weight += extra
|
||||
fourStarExtra += extra
|
||||
} else {
|
||||
nonFourStar += r.Weight
|
||||
}
|
||||
}
|
||||
if nonFourStar > 0 && fourStarExtra > 0 {
|
||||
for i, r := range adjusted {
|
||||
if r.RarityType < model.RaritySSRare {
|
||||
reduction := fourStarExtra * r.Weight / nonFourStar
|
||||
adjusted[i].Weight -= reduction
|
||||
if adjusted[i].Weight < 1 {
|
||||
adjusted[i].Weight = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return adjusted
|
||||
}
|
||||
|
||||
func rollOne(bp *masterdata.BannerPool, rates []RateTier, totalWeight int) DrawnItem {
|
||||
roll := rand.Intn(totalWeight)
|
||||
cumulative := 0
|
||||
var tier RateTier
|
||||
for _, r := range rates {
|
||||
cumulative += r.Weight
|
||||
if roll < cumulative {
|
||||
tier = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if item, ok := tryFeaturedRateUp(bp, tier); ok {
|
||||
return item
|
||||
}
|
||||
return pickFromPool(bp, tier.PossessionType, tier.RarityType)
|
||||
}
|
||||
|
||||
func tryFeaturedRateUp(bp *masterdata.BannerPool, tier RateTier) (DrawnItem, bool) {
|
||||
var matches []masterdata.GachaPoolItem
|
||||
for _, f := range bp.Featured {
|
||||
if f.PossessionType == tier.PossessionType && f.RarityType == tier.RarityType {
|
||||
matches = append(matches, f)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return DrawnItem{}, false
|
||||
}
|
||||
if rand.Intn(model.FeaturedRateUpDenom) >= model.FeaturedRateUpPercent {
|
||||
return DrawnItem{}, false
|
||||
}
|
||||
f := matches[rand.Intn(len(matches))]
|
||||
return DrawnItem{
|
||||
PossessionType: f.PossessionType,
|
||||
PossessionId: f.PossessionId,
|
||||
RarityType: f.RarityType,
|
||||
CharacterId: f.CharacterId,
|
||||
}, true
|
||||
}
|
||||
|
||||
func rollAtMinRarity(bp *masterdata.BannerPool, rates []RateTier, minRarity model.RarityType) DrawnItem {
|
||||
var filtered []RateTier
|
||||
filteredTotal := 0
|
||||
for _, r := range rates {
|
||||
if r.RarityType >= minRarity {
|
||||
filtered = append(filtered, r)
|
||||
filteredTotal += r.Weight
|
||||
}
|
||||
}
|
||||
if filteredTotal == 0 {
|
||||
return pickFromPool(bp, int32(model.PossessionTypeWeapon), minRarity)
|
||||
}
|
||||
return rollOne(bp, filtered, filteredTotal)
|
||||
}
|
||||
|
||||
func pickFromPool(bp *masterdata.BannerPool, possessionType int32, rarityType model.RarityType) DrawnItem {
|
||||
if possessionType == int32(model.PossessionTypeCostume) {
|
||||
items := bp.CostumesByRarity[rarityType]
|
||||
if len(items) == 0 {
|
||||
items = bp.CostumesByRarity[model.RaritySSRare]
|
||||
}
|
||||
if len(items) == 0 {
|
||||
log.Printf("[pickFromPool] empty costume pool for rarity=%d, returning phantom item", rarityType)
|
||||
return DrawnItem{PossessionType: int32(model.PossessionTypeWeapon), RarityType: rarityType}
|
||||
}
|
||||
pick := items[rand.Intn(len(items))]
|
||||
return DrawnItem{
|
||||
PossessionType: pick.PossessionType,
|
||||
PossessionId: pick.PossessionId,
|
||||
RarityType: pick.RarityType,
|
||||
CharacterId: pick.CharacterId,
|
||||
}
|
||||
}
|
||||
|
||||
items := bp.WeaponsByRarity[rarityType]
|
||||
if len(items) == 0 {
|
||||
items = bp.WeaponsByRarity[model.RarityRare]
|
||||
}
|
||||
if len(items) == 0 {
|
||||
log.Printf("[pickFromPool] empty weapon pool for rarity=%d, returning phantom item", rarityType)
|
||||
return DrawnItem{PossessionType: int32(model.PossessionTypeWeapon), RarityType: rarityType}
|
||||
}
|
||||
pick := items[rand.Intn(len(items))]
|
||||
return DrawnItem{
|
||||
PossessionType: pick.PossessionType,
|
||||
PossessionId: pick.PossessionId,
|
||||
RarityType: pick.RarityType,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package gacha
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
type DrawResult struct {
|
||||
Items []DrawnItem
|
||||
BonusItems map[int]DrawnItem
|
||||
Bonuses []store.GachaBonusEntry
|
||||
DuplicateInfos []DuplicateInfo
|
||||
BonusDuplicateInfos []DuplicateInfo
|
||||
MedalBonus int32
|
||||
}
|
||||
|
||||
type DuplicateInfo struct {
|
||||
Index int
|
||||
Grade int32
|
||||
Bonuses []model.DupExchangeEntry
|
||||
}
|
||||
|
||||
type GachaHandler struct {
|
||||
Pool *masterdata.GachaCatalog
|
||||
Config *masterdata.GameConfig
|
||||
Granter *store.PossessionGranter
|
||||
MedalInfo map[int32]masterdata.GachaMedalInfo
|
||||
DupExchange map[int32][]model.DupExchangeEntry
|
||||
}
|
||||
|
||||
func NewGachaHandler(
|
||||
pool *masterdata.GachaCatalog,
|
||||
config *masterdata.GameConfig,
|
||||
granter *store.PossessionGranter,
|
||||
medalInfo map[int32]masterdata.GachaMedalInfo,
|
||||
dupExchange map[int32][]model.DupExchangeEntry,
|
||||
) *GachaHandler {
|
||||
return &GachaHandler{
|
||||
Pool: pool,
|
||||
Config: config,
|
||||
Granter: granter,
|
||||
MedalInfo: medalInfo,
|
||||
DupExchange: dupExchange,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *GachaHandler) HandleDraw(
|
||||
user *store.UserState,
|
||||
entry store.GachaCatalogEntry,
|
||||
phaseId int32,
|
||||
execCount int32,
|
||||
) (*DrawResult, error) {
|
||||
phase, err := findPhase(entry, phaseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalCost := phase.Price * execCount
|
||||
if totalCost > 0 {
|
||||
if err := store.DeductPrice(user, phase.PriceType, phase.PriceId, totalCost); err != nil {
|
||||
log.Printf("[GachaHandler] DeductPrice failed (proceeding): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
drawCount := int(phase.DrawCount * execCount)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
bs := user.Gacha.BannerStates[entry.GachaId]
|
||||
bs.GachaId = entry.GachaId
|
||||
|
||||
var items []DrawnItem
|
||||
|
||||
switch entry.GachaLabelType {
|
||||
case model.GachaLabelPremium:
|
||||
items = h.drawPremium(entry, phase, drawCount)
|
||||
case model.GachaLabelChapter, model.GachaLabelRecycle:
|
||||
items = h.drawMaterial(drawCount)
|
||||
case model.GachaLabelEvent:
|
||||
items = h.drawBox(&bs, drawCount)
|
||||
default:
|
||||
items = h.drawPremium(entry, phase, drawCount)
|
||||
}
|
||||
|
||||
if entry.GachaModeType == model.GachaModeStepup {
|
||||
bs.StepNumber++
|
||||
if bs.StepNumber > entry.MaxStepNumber {
|
||||
bs.StepNumber = 1
|
||||
bs.LoopCount++
|
||||
}
|
||||
}
|
||||
|
||||
var medalBonus int32
|
||||
if entry.GachaMedalId != 0 {
|
||||
medalBonus = int32(drawCount)
|
||||
bs.MedalCount += medalBonus
|
||||
if bs.MedalCount > model.MedalCountCap {
|
||||
bs.MedalCount = model.MedalCountCap
|
||||
}
|
||||
}
|
||||
|
||||
bs.DrawCount += int32(drawCount)
|
||||
user.Gacha.BannerStates[entry.GachaId] = bs
|
||||
|
||||
dupInfos := h.grantItems(user, items, nowMillis)
|
||||
|
||||
bonusMap := h.generateBonusItems(entry, items)
|
||||
bonusSlice := make([]DrawnItem, 0, len(bonusMap))
|
||||
for _, b := range bonusMap {
|
||||
bonusSlice = append(bonusSlice, b)
|
||||
}
|
||||
bonusDupInfos := h.grantItems(user, bonusSlice, nowMillis)
|
||||
|
||||
result := &DrawResult{
|
||||
Items: items,
|
||||
BonusItems: bonusMap,
|
||||
DuplicateInfos: dupInfos,
|
||||
BonusDuplicateInfos: bonusDupInfos,
|
||||
MedalBonus: medalBonus,
|
||||
}
|
||||
|
||||
for _, p := range phase.Bonuses {
|
||||
store.GrantPossession(user, model.PossessionType(p.PossessionType), p.PossessionId, p.Count)
|
||||
result.Bonuses = append(result.Bonuses, p)
|
||||
}
|
||||
|
||||
if medalBonus > 0 && entry.MedalConsumableItemId != 0 {
|
||||
store.GrantPossession(user, model.PossessionTypeConsumableItem, entry.MedalConsumableItemId, medalBonus)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *GachaHandler) HandleResetBox(
|
||||
user *store.UserState,
|
||||
entry store.GachaCatalogEntry,
|
||||
) error {
|
||||
bs := user.Gacha.BannerStates[entry.GachaId]
|
||||
bs.BoxDrewCounts = make(map[int32]int32)
|
||||
bs.BoxNumber++
|
||||
user.Gacha.BannerStates[entry.GachaId] = bs
|
||||
return nil
|
||||
}
|
||||
|
||||
func clampDailyDraw(lastDate, todayStart int64, currentCount, maxCount, requested int32) (clamped, newCount int32, reset bool) {
|
||||
if lastDate < todayStart {
|
||||
currentCount = 0
|
||||
reset = true
|
||||
}
|
||||
remaining := maxCount - currentCount
|
||||
if remaining <= 0 {
|
||||
return 0, currentCount, reset
|
||||
}
|
||||
if requested > remaining {
|
||||
requested = remaining
|
||||
}
|
||||
return requested, currentCount + requested, reset
|
||||
}
|
||||
|
||||
func (h *GachaHandler) HandleRewardDraw(
|
||||
user *store.UserState,
|
||||
count int32,
|
||||
) ([]DrawnItem, error) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
todayStart := gametime.StartOfDayMillis()
|
||||
|
||||
maxCount := h.Config.RewardGachaDailyMaxCount
|
||||
if maxCount <= 0 {
|
||||
maxCount = model.DefaultDailyDrawLimit
|
||||
}
|
||||
|
||||
clamped, newCount, _ := clampDailyDraw(
|
||||
user.Gacha.LastRewardDrawDate, todayStart,
|
||||
user.Gacha.TodaysCurrentDrawCount, maxCount, count,
|
||||
)
|
||||
if clamped <= 0 {
|
||||
return nil, fmt.Errorf("daily reward draw limit reached")
|
||||
}
|
||||
|
||||
items := DrawReward(h.Pool.Materials, int(clamped))
|
||||
|
||||
for _, item := range items {
|
||||
store.GrantPossession(user, model.PossessionType(item.PossessionType), item.PossessionId, 1)
|
||||
}
|
||||
|
||||
user.Gacha.TodaysCurrentDrawCount = newCount
|
||||
user.Gacha.DailyMaxCount = maxCount
|
||||
user.Gacha.LastRewardDrawDate = nowMillis
|
||||
user.Gacha.RewardAvailable = newCount < maxCount
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (h *GachaHandler) drawPremium(entry store.GachaCatalogEntry, phase store.GachaPricePhaseEntry, count int) []DrawnItem {
|
||||
fixedMin := phase.FixedRarityMin
|
||||
fixedCount := int(phase.FixedCount)
|
||||
|
||||
bp := h.Pool.BannerPools[entry.GachaId]
|
||||
if bp == nil {
|
||||
bp = &masterdata.BannerPool{
|
||||
CostumesByRarity: h.Pool.CostumesByRarity,
|
||||
WeaponsByRarity: h.Pool.WeaponsByRarity,
|
||||
}
|
||||
}
|
||||
|
||||
rateMultiplier := 1.0
|
||||
if entry.GachaModeType == model.GachaModeStepup {
|
||||
switch phase.StepNumber {
|
||||
case 1, 3:
|
||||
rateMultiplier = model.StepUpRateBoost
|
||||
case 5:
|
||||
rateMultiplier = model.StepUpRateMaxBoost
|
||||
}
|
||||
}
|
||||
|
||||
return DrawPremium(bp, count, fixedMin, fixedCount, rateMultiplier)
|
||||
}
|
||||
|
||||
func (h *GachaHandler) drawMaterial(count int) []DrawnItem {
|
||||
return DrawReward(h.Pool.Materials, count)
|
||||
}
|
||||
|
||||
func (h *GachaHandler) drawBox(bs *store.GachaBannerState, count int) []DrawnItem {
|
||||
if bs.BoxDrewCounts == nil {
|
||||
bs.BoxDrewCounts = make(map[int32]int32)
|
||||
}
|
||||
|
||||
boxItems := h.buildBoxPool()
|
||||
for i := range boxItems {
|
||||
boxItems[i].DrewCount = bs.BoxDrewCounts[boxItems[i].PossessionId]
|
||||
}
|
||||
|
||||
result := DrawBox(boxItems, count)
|
||||
|
||||
for _, item := range result {
|
||||
bs.BoxDrewCounts[item.PossessionId]++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *GachaHandler) buildBoxPool() []BoxItem {
|
||||
var items []BoxItem
|
||||
for _, mat := range h.Pool.Materials {
|
||||
items = append(items, BoxItem{
|
||||
PossessionType: mat.PossessionType,
|
||||
PossessionId: mat.PossessionId,
|
||||
RarityType: mat.RarityType,
|
||||
Count: 1,
|
||||
MaxCount: model.BoxItemDefaultMax,
|
||||
})
|
||||
if len(items) >= model.BoxPoolMaxItems {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(items) < model.BoxPoolMinItems {
|
||||
items = append(items, BoxItem{
|
||||
PossessionType: int32(model.PossessionTypeMaterial),
|
||||
PossessionId: model.BoxFallbackItemId,
|
||||
RarityType: model.RarityNormal,
|
||||
Count: 1,
|
||||
MaxCount: model.BoxFallbackItemMax,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (h *GachaHandler) grantItems(user *store.UserState, items []DrawnItem, nowMillis int64) []DuplicateInfo {
|
||||
var dupInfos []DuplicateInfo
|
||||
for i, item := range items {
|
||||
switch model.PossessionType(item.PossessionType) {
|
||||
case model.PossessionTypeCostume:
|
||||
if dup, ok := h.tryCostumeDupExchange(user, item, i); ok {
|
||||
dupInfos = append(dupInfos, dup)
|
||||
continue
|
||||
}
|
||||
h.Granter.GrantCostume(user, item.PossessionId, nowMillis)
|
||||
case model.PossessionTypeWeapon:
|
||||
h.Granter.GrantWeapon(user, item.PossessionId, nowMillis)
|
||||
default:
|
||||
if item.PossessionType != 0 {
|
||||
store.GrantPossession(user, model.PossessionType(item.PossessionType), item.PossessionId, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dupInfos
|
||||
}
|
||||
|
||||
func (h *GachaHandler) tryCostumeDupExchange(user *store.UserState, item DrawnItem, index int) (DuplicateInfo, bool) {
|
||||
for _, c := range user.Costumes {
|
||||
if c.CostumeId == item.PossessionId {
|
||||
grade := int32(rand.Intn(model.DupGradeRange) + int(model.DupGradeMin))
|
||||
exchanges := h.DupExchange[item.PossessionId]
|
||||
for _, ex := range exchanges {
|
||||
store.GrantPossession(user, model.PossessionType(ex.PossessionType), ex.PossessionId, ex.Count)
|
||||
}
|
||||
return DuplicateInfo{Index: index, Grade: grade, Bonuses: exchanges}, true
|
||||
}
|
||||
}
|
||||
return DuplicateInfo{}, false
|
||||
}
|
||||
|
||||
func (h *GachaHandler) generateBonusItems(entry store.GachaCatalogEntry, mainItems []DrawnItem) map[int]DrawnItem {
|
||||
bonus := make(map[int]DrawnItem)
|
||||
for i, item := range mainItems {
|
||||
if item.PossessionType != int32(model.PossessionTypeCostume) {
|
||||
continue
|
||||
}
|
||||
wid, ok := h.Pool.CostumeWeaponMap[item.PossessionId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
w, ok := h.Pool.WeaponById[wid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
bonus[i] = DrawnItem{
|
||||
PossessionType: w.PossessionType,
|
||||
PossessionId: w.PossessionId,
|
||||
RarityType: w.RarityType,
|
||||
}
|
||||
}
|
||||
return bonus
|
||||
}
|
||||
|
||||
func findPhase(entry store.GachaCatalogEntry, phaseId int32) (store.GachaPricePhaseEntry, error) {
|
||||
for _, p := range entry.PricePhases {
|
||||
if p.PhaseId == phaseId {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
if len(entry.PricePhases) > 0 {
|
||||
log.Printf("[GachaHandler] phase %d not found for gacha %d, using first phase", phaseId, entry.GachaId)
|
||||
return entry.PricePhases[0], nil
|
||||
}
|
||||
return store.GachaPricePhaseEntry{}, fmt.Errorf("no price phases for gacha %d", entry.GachaId)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gametime
|
||||
|
||||
import "time"
|
||||
|
||||
func Now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func NowMillis() int64 {
|
||||
return Now().UnixMilli()
|
||||
}
|
||||
|
||||
func StartOfDayMillis() int64 {
|
||||
n := Now()
|
||||
return time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||
}
|
||||
|
||||
// WeeklyVersion returns a stable weekly identifier (start-of-week timestamp in millis, Monday 00:00 UTC).
|
||||
func WeeklyVersion(millis int64) int64 {
|
||||
t := time.UnixMilli(millis).UTC()
|
||||
weekday := int(t.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
monday := time.Date(t.Year(), t.Month(), t.Day()-(weekday-1), 0, 0, 0, 0, time.UTC)
|
||||
return monday.UnixMilli()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package gameutil
|
||||
|
||||
// LevelAndCap returns the level for the given cumulative EXP based on
|
||||
// the thresholds slice, and clamps EXP so it never exceeds the last
|
||||
// threshold (max-level cap).
|
||||
func LevelAndCap(exp int32, thresholds []int32) (level, capped int32) {
|
||||
level = 1
|
||||
for lvl := 1; lvl < len(thresholds); lvl++ {
|
||||
if exp >= thresholds[lvl] {
|
||||
level = int32(lvl)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(thresholds) > 0 && exp > thresholds[len(thresholds)-1] {
|
||||
exp = thresholds[len(thresholds)-1]
|
||||
}
|
||||
return level, exp
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type bigHuntBossQuestRow struct {
|
||||
BigHuntBossQuestId int32 `json:"BigHuntBossQuestId"`
|
||||
BigHuntBossId int32 `json:"BigHuntBossId"`
|
||||
BigHuntQuestGroupId int32 `json:"BigHuntQuestGroupId"`
|
||||
BigHuntBossQuestScoreCoefficientId int32 `json:"BigHuntBossQuestScoreCoefficientId"`
|
||||
BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"`
|
||||
DailyChallengeCount int32 `json:"DailyChallengeCount"`
|
||||
}
|
||||
|
||||
type BigHuntBossQuestRow struct {
|
||||
BigHuntBossQuestId int32
|
||||
BigHuntBossId int32
|
||||
BigHuntQuestGroupId int32
|
||||
BigHuntScoreRewardGroupScheduleId int32
|
||||
DailyChallengeCount int32
|
||||
}
|
||||
|
||||
type bigHuntQuestRow struct {
|
||||
BigHuntQuestId int32 `json:"BigHuntQuestId"`
|
||||
QuestId int32 `json:"QuestId"`
|
||||
BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"`
|
||||
}
|
||||
|
||||
type BigHuntQuestRow struct {
|
||||
BigHuntQuestId int32
|
||||
QuestId int32
|
||||
BigHuntQuestScoreCoefficientId int32
|
||||
}
|
||||
|
||||
type bigHuntQuestScoreCoefficientRow struct {
|
||||
BigHuntQuestScoreCoefficientId int32 `json:"BigHuntQuestScoreCoefficientId"`
|
||||
ScoreDifficultBonusPermil int32 `json:"ScoreDifficultBonusPermil"`
|
||||
}
|
||||
|
||||
type bigHuntBossRow struct {
|
||||
BigHuntBossId int32 `json:"BigHuntBossId"`
|
||||
BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"`
|
||||
AttributeType int32 `json:"AttributeType"`
|
||||
}
|
||||
|
||||
type BigHuntBossRow struct {
|
||||
BigHuntBossId int32
|
||||
BigHuntBossGradeGroupId int32
|
||||
AttributeType int32
|
||||
}
|
||||
|
||||
type bigHuntBossGradeGroupRow struct {
|
||||
BigHuntBossGradeGroupId int32 `json:"BigHuntBossGradeGroupId"`
|
||||
NecessaryScore int64 `json:"NecessaryScore"`
|
||||
AssetGradeIconId int32 `json:"AssetGradeIconId"`
|
||||
}
|
||||
|
||||
type GradeThreshold struct {
|
||||
NecessaryScore int64
|
||||
AssetGradeIconId int32
|
||||
}
|
||||
|
||||
type bigHuntScheduleRow struct {
|
||||
BigHuntScheduleId int32 `json:"BigHuntScheduleId"`
|
||||
ChallengeStartDatetime int64 `json:"ChallengeStartDatetime"`
|
||||
ChallengeEndDatetime int64 `json:"ChallengeEndDatetime"`
|
||||
}
|
||||
|
||||
type scoreRewardScheduleRow struct {
|
||||
BigHuntScoreRewardGroupScheduleId int32 `json:"BigHuntScoreRewardGroupScheduleId"`
|
||||
GroupIndex int32 `json:"GroupIndex"`
|
||||
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
|
||||
StartDatetime int64 `json:"StartDatetime"`
|
||||
}
|
||||
|
||||
type ScoreRewardScheduleEntry struct {
|
||||
BigHuntScoreRewardGroupId int32
|
||||
StartDatetime int64
|
||||
}
|
||||
|
||||
type scoreRewardGroupRow struct {
|
||||
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
|
||||
NecessaryScore int64 `json:"NecessaryScore"`
|
||||
BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"`
|
||||
}
|
||||
|
||||
type ScoreRewardThreshold struct {
|
||||
NecessaryScore int64
|
||||
BigHuntRewardGroupId int32
|
||||
}
|
||||
|
||||
type rewardGroupRow struct {
|
||||
BigHuntRewardGroupId int32 `json:"BigHuntRewardGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type RewardItem struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
type weeklyRewardScheduleRow struct {
|
||||
BigHuntWeeklyAttributeScoreRewardGroupScheduleId int32 `json:"BigHuntWeeklyAttributeScoreRewardGroupScheduleId"`
|
||||
AttributeType int32 `json:"AttributeType"`
|
||||
GroupIndex int32 `json:"GroupIndex"`
|
||||
BigHuntScoreRewardGroupId int32 `json:"BigHuntScoreRewardGroupId"`
|
||||
StartDatetime int64 `json:"StartDatetime"`
|
||||
}
|
||||
|
||||
type BigHuntWeeklyRewardKey struct {
|
||||
ScheduleId int32
|
||||
AttributeType int32
|
||||
}
|
||||
|
||||
type BigHuntCatalog struct {
|
||||
BossQuestById map[int32]BigHuntBossQuestRow
|
||||
QuestById map[int32]BigHuntQuestRow
|
||||
ScoreCoefficients map[int32]int32
|
||||
BossByBossId map[int32]BigHuntBossRow
|
||||
GradeThresholds map[int32][]GradeThreshold
|
||||
ActiveScheduleId int32
|
||||
ScoreRewardSchedules map[int32][]ScoreRewardScheduleEntry
|
||||
ScoreRewardThresholds map[int32][]ScoreRewardThreshold
|
||||
RewardItems map[int32][]RewardItem
|
||||
WeeklyRewardSchedules map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry
|
||||
}
|
||||
|
||||
func (c *BigHuntCatalog) ResolveActiveScoreRewardGroupId(scheduleId int32, nowMillis int64) int32 {
|
||||
entries := c.ScoreRewardSchedules[scheduleId]
|
||||
for _, e := range entries {
|
||||
if nowMillis >= e.StartDatetime {
|
||||
return e.BigHuntScoreRewardGroupId
|
||||
}
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return entries[len(entries)-1].BigHuntScoreRewardGroupId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *BigHuntCatalog) ResolveActiveWeeklyRewardGroupId(key BigHuntWeeklyRewardKey, nowMillis int64) int32 {
|
||||
entries := c.WeeklyRewardSchedules[key]
|
||||
for _, e := range entries {
|
||||
if nowMillis >= e.StartDatetime {
|
||||
return e.BigHuntScoreRewardGroupId
|
||||
}
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return entries[len(entries)-1].BigHuntScoreRewardGroupId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *BigHuntCatalog) ResolveGradeIconId(bossId int32, score int64) int32 {
|
||||
boss, ok := c.BossByBossId[bossId]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
thresholds := c.GradeThresholds[boss.BigHuntBossGradeGroupId]
|
||||
var iconId int32
|
||||
for _, t := range thresholds {
|
||||
if score >= t.NecessaryScore {
|
||||
iconId = t.AssetGradeIconId
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return iconId
|
||||
}
|
||||
|
||||
func (c *BigHuntCatalog) CollectNewRewards(scoreRewardGroupId int32, oldMax, newMax int64) []RewardItem {
|
||||
thresholds := c.ScoreRewardThresholds[scoreRewardGroupId]
|
||||
var items []RewardItem
|
||||
for _, t := range thresholds {
|
||||
if t.NecessaryScore > oldMax && t.NecessaryScore <= newMax {
|
||||
items = append(items, c.RewardItems[t.BigHuntRewardGroupId]...)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func LoadBigHuntCatalog() *BigHuntCatalog {
|
||||
bossQuestRows, err := utils.ReadJSON[bigHuntBossQuestRow]("EntityMBigHuntBossQuestTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt boss quest table: %v", err)
|
||||
}
|
||||
bossQuestById := make(map[int32]BigHuntBossQuestRow, len(bossQuestRows))
|
||||
for _, r := range bossQuestRows {
|
||||
bossQuestById[r.BigHuntBossQuestId] = BigHuntBossQuestRow{
|
||||
BigHuntBossQuestId: r.BigHuntBossQuestId,
|
||||
BigHuntBossId: r.BigHuntBossId,
|
||||
BigHuntQuestGroupId: r.BigHuntQuestGroupId,
|
||||
BigHuntScoreRewardGroupScheduleId: r.BigHuntScoreRewardGroupScheduleId,
|
||||
DailyChallengeCount: r.DailyChallengeCount,
|
||||
}
|
||||
}
|
||||
|
||||
questRows, err := utils.ReadJSON[bigHuntQuestRow]("EntityMBigHuntQuestTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt quest table: %v", err)
|
||||
}
|
||||
questById := make(map[int32]BigHuntQuestRow, len(questRows))
|
||||
for _, r := range questRows {
|
||||
questById[r.BigHuntQuestId] = BigHuntQuestRow{
|
||||
BigHuntQuestId: r.BigHuntQuestId,
|
||||
QuestId: r.QuestId,
|
||||
BigHuntQuestScoreCoefficientId: r.BigHuntQuestScoreCoefficientId,
|
||||
}
|
||||
}
|
||||
|
||||
coeffRows, err := utils.ReadJSON[bigHuntQuestScoreCoefficientRow]("EntityMBigHuntQuestScoreCoefficientTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt quest score coefficient table: %v", err)
|
||||
}
|
||||
scoreCoefficients := make(map[int32]int32, len(coeffRows))
|
||||
for _, r := range coeffRows {
|
||||
scoreCoefficients[r.BigHuntQuestScoreCoefficientId] = r.ScoreDifficultBonusPermil
|
||||
}
|
||||
|
||||
bossRows, err := utils.ReadJSON[bigHuntBossRow]("EntityMBigHuntBossTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt boss table: %v", err)
|
||||
}
|
||||
bossByBossId := make(map[int32]BigHuntBossRow, len(bossRows))
|
||||
for _, r := range bossRows {
|
||||
bossByBossId[r.BigHuntBossId] = BigHuntBossRow{
|
||||
BigHuntBossId: r.BigHuntBossId,
|
||||
BigHuntBossGradeGroupId: r.BigHuntBossGradeGroupId,
|
||||
AttributeType: r.AttributeType,
|
||||
}
|
||||
}
|
||||
|
||||
gradeRows, err := utils.ReadJSON[bigHuntBossGradeGroupRow]("EntityMBigHuntBossGradeGroupTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt boss grade group table: %v", err)
|
||||
}
|
||||
gradeThresholds := make(map[int32][]GradeThreshold)
|
||||
for _, r := range gradeRows {
|
||||
gradeThresholds[r.BigHuntBossGradeGroupId] = append(gradeThresholds[r.BigHuntBossGradeGroupId], GradeThreshold{
|
||||
NecessaryScore: r.NecessaryScore,
|
||||
AssetGradeIconId: r.AssetGradeIconId,
|
||||
})
|
||||
}
|
||||
for k := range gradeThresholds {
|
||||
sort.Slice(gradeThresholds[k], func(i, j int) bool {
|
||||
return gradeThresholds[k][i].NecessaryScore < gradeThresholds[k][j].NecessaryScore
|
||||
})
|
||||
}
|
||||
|
||||
scheduleRows, err := utils.ReadJSON[bigHuntScheduleRow]("EntityMBigHuntScheduleTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt schedule table: %v", err)
|
||||
}
|
||||
nowMillis := time.Now().UnixMilli()
|
||||
var activeScheduleId int32
|
||||
var latestEndDatetime int64
|
||||
for _, r := range scheduleRows {
|
||||
if nowMillis >= r.ChallengeStartDatetime && nowMillis <= r.ChallengeEndDatetime {
|
||||
activeScheduleId = r.BigHuntScheduleId
|
||||
break
|
||||
}
|
||||
if r.ChallengeEndDatetime > latestEndDatetime {
|
||||
latestEndDatetime = r.ChallengeEndDatetime
|
||||
activeScheduleId = r.BigHuntScheduleId
|
||||
}
|
||||
}
|
||||
|
||||
rewardSchedRows, err := utils.ReadJSON[scoreRewardScheduleRow]("EntityMBigHuntScoreRewardGroupScheduleTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt score reward group schedule table: %v", err)
|
||||
}
|
||||
scoreRewardSchedules := make(map[int32][]ScoreRewardScheduleEntry)
|
||||
for _, r := range rewardSchedRows {
|
||||
scoreRewardSchedules[r.BigHuntScoreRewardGroupScheduleId] = append(
|
||||
scoreRewardSchedules[r.BigHuntScoreRewardGroupScheduleId],
|
||||
ScoreRewardScheduleEntry{
|
||||
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
|
||||
StartDatetime: r.StartDatetime,
|
||||
},
|
||||
)
|
||||
}
|
||||
for k := range scoreRewardSchedules {
|
||||
sort.Slice(scoreRewardSchedules[k], func(i, j int) bool {
|
||||
return scoreRewardSchedules[k][i].StartDatetime > scoreRewardSchedules[k][j].StartDatetime
|
||||
})
|
||||
}
|
||||
|
||||
rewardGroupRows, err := utils.ReadJSON[scoreRewardGroupRow]("EntityMBigHuntScoreRewardGroupTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt score reward group table: %v", err)
|
||||
}
|
||||
scoreRewardThresholds := make(map[int32][]ScoreRewardThreshold)
|
||||
for _, r := range rewardGroupRows {
|
||||
scoreRewardThresholds[r.BigHuntScoreRewardGroupId] = append(
|
||||
scoreRewardThresholds[r.BigHuntScoreRewardGroupId],
|
||||
ScoreRewardThreshold{
|
||||
NecessaryScore: r.NecessaryScore,
|
||||
BigHuntRewardGroupId: r.BigHuntRewardGroupId,
|
||||
},
|
||||
)
|
||||
}
|
||||
for k := range scoreRewardThresholds {
|
||||
sort.Slice(scoreRewardThresholds[k], func(i, j int) bool {
|
||||
return scoreRewardThresholds[k][i].NecessaryScore < scoreRewardThresholds[k][j].NecessaryScore
|
||||
})
|
||||
}
|
||||
|
||||
rewardItemRows, err := utils.ReadJSON[rewardGroupRow]("EntityMBigHuntRewardGroupTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt reward group table: %v", err)
|
||||
}
|
||||
rewardItems := make(map[int32][]RewardItem)
|
||||
for _, r := range rewardItemRows {
|
||||
rewardItems[r.BigHuntRewardGroupId] = append(rewardItems[r.BigHuntRewardGroupId], RewardItem{
|
||||
PossessionType: r.PossessionType,
|
||||
PossessionId: r.PossessionId,
|
||||
Count: r.Count,
|
||||
})
|
||||
}
|
||||
|
||||
weeklySchedRows, err := utils.ReadJSON[weeklyRewardScheduleRow]("EntityMBigHuntWeeklyAttributeScoreRewardGroupScheduleTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load big hunt weekly attribute score reward group schedule table: %v", err)
|
||||
}
|
||||
weeklyRewardSchedules := make(map[BigHuntWeeklyRewardKey][]ScoreRewardScheduleEntry)
|
||||
for _, r := range weeklySchedRows {
|
||||
key := BigHuntWeeklyRewardKey{
|
||||
ScheduleId: r.BigHuntWeeklyAttributeScoreRewardGroupScheduleId,
|
||||
AttributeType: r.AttributeType,
|
||||
}
|
||||
weeklyRewardSchedules[key] = append(weeklyRewardSchedules[key], ScoreRewardScheduleEntry{
|
||||
BigHuntScoreRewardGroupId: r.BigHuntScoreRewardGroupId,
|
||||
StartDatetime: r.StartDatetime,
|
||||
})
|
||||
}
|
||||
for k := range weeklyRewardSchedules {
|
||||
sort.Slice(weeklyRewardSchedules[k], func(i, j int) bool {
|
||||
return weeklyRewardSchedules[k][i].StartDatetime > weeklyRewardSchedules[k][j].StartDatetime
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("big hunt catalog loaded: %d boss quests, %d quests, %d bosses, %d score coefficients, %d reward groups, schedule=%d",
|
||||
len(bossQuestById), len(questById), len(bossByBossId), len(scoreCoefficients), len(rewardItems), activeScheduleId)
|
||||
|
||||
return &BigHuntCatalog{
|
||||
BossQuestById: bossQuestById,
|
||||
QuestById: questById,
|
||||
ScoreCoefficients: scoreCoefficients,
|
||||
BossByBossId: bossByBossId,
|
||||
GradeThresholds: gradeThresholds,
|
||||
ActiveScheduleId: activeScheduleId,
|
||||
ScoreRewardSchedules: scoreRewardSchedules,
|
||||
ScoreRewardThresholds: scoreRewardThresholds,
|
||||
RewardItems: rewardItems,
|
||||
WeeklyRewardSchedules: weeklyRewardSchedules,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type cageOrnament struct {
|
||||
CageOrnamentId int32 `json:"CageOrnamentId"`
|
||||
CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"`
|
||||
}
|
||||
|
||||
type cageOrnamentRewardRow struct {
|
||||
CageOrnamentRewardId int32 `json:"CageOrnamentRewardId"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type CageOrnamentReward struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
type CageOrnamentCatalog struct {
|
||||
ornamentToRewardId map[int32]int32
|
||||
rewards map[int32]CageOrnamentReward
|
||||
}
|
||||
|
||||
func (c *CageOrnamentCatalog) LookupReward(cageOrnamentId int32) (CageOrnamentReward, bool) {
|
||||
rewardId, ok := c.ornamentToRewardId[cageOrnamentId]
|
||||
if !ok || rewardId == 0 {
|
||||
return CageOrnamentReward{}, false
|
||||
}
|
||||
entry, ok := c.rewards[rewardId]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func LoadCageOrnamentCatalog() *CageOrnamentCatalog {
|
||||
ornaments, err := utils.ReadJSON[cageOrnament]("EntityMCageOrnamentTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load cage ornament table: %v", err)
|
||||
}
|
||||
rewards, err := utils.ReadJSON[cageOrnamentRewardRow]("EntityMCageOrnamentRewardTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load cage ornament reward table: %v", err)
|
||||
}
|
||||
|
||||
cat := &CageOrnamentCatalog{
|
||||
ornamentToRewardId: make(map[int32]int32, len(ornaments)),
|
||||
rewards: make(map[int32]CageOrnamentReward, len(rewards)),
|
||||
}
|
||||
for _, o := range ornaments {
|
||||
cat.ornamentToRewardId[o.CageOrnamentId] = o.CageOrnamentRewardId
|
||||
}
|
||||
for _, r := range rewards {
|
||||
cat.rewards[r.CageOrnamentRewardId] = CageOrnamentReward{
|
||||
PossessionType: r.PossessionType,
|
||||
PossessionId: r.PossessionId,
|
||||
Count: r.Count,
|
||||
}
|
||||
}
|
||||
return cat
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type CharacterRebirthRow struct {
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"`
|
||||
}
|
||||
|
||||
type CharacterRebirthStepRow struct {
|
||||
CharacterRebirthStepGroupId int32 `json:"CharacterRebirthStepGroupId"`
|
||||
BeforeRebirthCount int32 `json:"BeforeRebirthCount"`
|
||||
CostumeLevelLimitUp int32 `json:"CostumeLevelLimitUp"`
|
||||
CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"`
|
||||
}
|
||||
|
||||
type CharacterRebirthMaterialRow struct {
|
||||
CharacterRebirthMaterialGroupId int32 `json:"CharacterRebirthMaterialGroupId"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type StepKey struct {
|
||||
GroupId int32
|
||||
BeforeRebirthCount int32
|
||||
}
|
||||
|
||||
type CharacterRebirthCatalog struct {
|
||||
StepGroupByCharacterId map[int32]int32
|
||||
StepByGroupAndCount map[StepKey]CharacterRebirthStepRow
|
||||
MaterialsByGroupId map[int32][]CharacterRebirthMaterialRow
|
||||
}
|
||||
|
||||
func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) {
|
||||
rebirthRows, err := utils.ReadJSON[CharacterRebirthRow]("EntityMCharacterRebirthTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character rebirth table: %w", err)
|
||||
}
|
||||
|
||||
stepRows, err := utils.ReadJSON[CharacterRebirthStepRow]("EntityMCharacterRebirthStepGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character rebirth step group table: %w", err)
|
||||
}
|
||||
|
||||
materialRows, err := utils.ReadJSON[CharacterRebirthMaterialRow]("EntityMCharacterRebirthMaterialGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character rebirth material group table: %w", err)
|
||||
}
|
||||
|
||||
stepGroupByCharacterId := make(map[int32]int32, len(rebirthRows))
|
||||
for _, r := range rebirthRows {
|
||||
stepGroupByCharacterId[r.CharacterId] = r.CharacterRebirthStepGroupId
|
||||
}
|
||||
|
||||
stepByGroupAndCount := make(map[StepKey]CharacterRebirthStepRow, len(stepRows))
|
||||
for _, s := range stepRows {
|
||||
stepByGroupAndCount[StepKey{GroupId: s.CharacterRebirthStepGroupId, BeforeRebirthCount: s.BeforeRebirthCount}] = s
|
||||
}
|
||||
|
||||
materialsByGroupId := make(map[int32][]CharacterRebirthMaterialRow)
|
||||
for _, m := range materialRows {
|
||||
materialsByGroupId[m.CharacterRebirthMaterialGroupId] = append(materialsByGroupId[m.CharacterRebirthMaterialGroupId], m)
|
||||
}
|
||||
|
||||
return &CharacterRebirthCatalog{
|
||||
StepGroupByCharacterId: stepGroupByCharacterId,
|
||||
StepByGroupAndCount: stepByGroupAndCount,
|
||||
MaterialsByGroupId: materialsByGroupId,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type CharacterBoardPanelRow struct {
|
||||
CharacterBoardPanelId int32 `json:"CharacterBoardPanelId"`
|
||||
CharacterBoardId int32 `json:"CharacterBoardId"`
|
||||
CharacterBoardPanelUnlockConditionGroupId int32 `json:"CharacterBoardPanelUnlockConditionGroupId"`
|
||||
CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"`
|
||||
CharacterBoardPanelReleaseRewardGroupId int32 `json:"CharacterBoardPanelReleaseRewardGroupId"`
|
||||
CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
ParentCharacterBoardPanelId int32 `json:"ParentCharacterBoardPanelId"`
|
||||
PlaceIndex int32 `json:"PlaceIndex"`
|
||||
}
|
||||
|
||||
type CharacterBoardReleasePossessionRow struct {
|
||||
CharacterBoardPanelReleasePossessionGroupId int32 `json:"CharacterBoardPanelReleasePossessionGroupId"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type CharacterBoardReleaseEffectRow struct {
|
||||
CharacterBoardPanelReleaseEffectGroupId int32 `json:"CharacterBoardPanelReleaseEffectGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
CharacterBoardEffectType int32 `json:"CharacterBoardEffectType"`
|
||||
CharacterBoardEffectId int32 `json:"CharacterBoardEffectId"`
|
||||
EffectValue int32 `json:"EffectValue"`
|
||||
}
|
||||
|
||||
type CharacterBoardRow struct {
|
||||
CharacterBoardId int32 `json:"CharacterBoardId"`
|
||||
CharacterBoardGroupId int32 `json:"CharacterBoardGroupId"`
|
||||
CharacterBoardUnlockConditionGroupId int32 `json:"CharacterBoardUnlockConditionGroupId"`
|
||||
ReleaseRank int32 `json:"ReleaseRank"`
|
||||
}
|
||||
|
||||
type CharacterBoardStatusUpRow struct {
|
||||
CharacterBoardStatusUpId int32 `json:"CharacterBoardStatusUpId"`
|
||||
CharacterBoardStatusUpType int32 `json:"CharacterBoardStatusUpType"`
|
||||
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
|
||||
}
|
||||
|
||||
type CharacterBoardAbilityRow struct {
|
||||
CharacterBoardAbilityId int32 `json:"CharacterBoardAbilityId"`
|
||||
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
|
||||
AbilityId int32 `json:"AbilityId"`
|
||||
}
|
||||
|
||||
type CharacterBoardAbilityMaxLevelRow struct {
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
AbilityId int32 `json:"AbilityId"`
|
||||
MaxLevel int32 `json:"MaxLevel"`
|
||||
}
|
||||
|
||||
type CharacterBoardEffectTargetRow struct {
|
||||
CharacterBoardEffectTargetGroupId int32 `json:"CharacterBoardEffectTargetGroupId"`
|
||||
GroupIndex int32 `json:"GroupIndex"`
|
||||
CharacterBoardEffectTargetType int32 `json:"CharacterBoardEffectTargetType"`
|
||||
TargetValue int32 `json:"TargetValue"`
|
||||
}
|
||||
|
||||
type CharacterBoardAssignmentRow struct {
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
CharacterBoardCategoryId int32 `json:"CharacterBoardCategoryId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
CharacterBoardAssignmentType int32 `json:"CharacterBoardAssignmentType"`
|
||||
}
|
||||
|
||||
type CharacterBoardGroupRow struct {
|
||||
CharacterBoardGroupId int32 `json:"CharacterBoardGroupId"`
|
||||
CharacterBoardCategoryId int32 `json:"CharacterBoardCategoryId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
CharacterBoardGroupType int32 `json:"CharacterBoardGroupType"`
|
||||
TextAssetId int32 `json:"TextAssetId"`
|
||||
}
|
||||
|
||||
type CharacterBoardCatalog struct {
|
||||
PanelById map[int32]CharacterBoardPanelRow
|
||||
PanelsByBoardId map[int32][]CharacterBoardPanelRow
|
||||
ReleaseCostsByGroupId map[int32][]CharacterBoardReleasePossessionRow
|
||||
ReleaseEffectsByGroupId map[int32][]CharacterBoardReleaseEffectRow
|
||||
StatusUpById map[int32]CharacterBoardStatusUpRow
|
||||
AbilityById map[int32]CharacterBoardAbilityRow
|
||||
AbilityMaxLevel map[store.CharacterBoardAbilityKey]int32
|
||||
EffectTargetsByGroupId map[int32][]CharacterBoardEffectTargetRow
|
||||
BoardById map[int32]CharacterBoardRow
|
||||
}
|
||||
|
||||
func LoadCharacterBoardCatalog() (*CharacterBoardCatalog, error) {
|
||||
panels, err := utils.ReadJSON[CharacterBoardPanelRow]("EntityMCharacterBoardPanelTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board panel table: %w", err)
|
||||
}
|
||||
|
||||
costs, err := utils.ReadJSON[CharacterBoardReleasePossessionRow]("EntityMCharacterBoardPanelReleasePossessionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board release possession table: %w", err)
|
||||
}
|
||||
|
||||
effects, err := utils.ReadJSON[CharacterBoardReleaseEffectRow]("EntityMCharacterBoardPanelReleaseEffectGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board release effect table: %w", err)
|
||||
}
|
||||
|
||||
boards, err := utils.ReadJSON[CharacterBoardRow]("EntityMCharacterBoardTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board table: %w", err)
|
||||
}
|
||||
|
||||
statusUps, err := utils.ReadJSON[CharacterBoardStatusUpRow]("EntityMCharacterBoardStatusUpTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board status up table: %w", err)
|
||||
}
|
||||
|
||||
abilities, err := utils.ReadJSON[CharacterBoardAbilityRow]("EntityMCharacterBoardAbilityTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board ability table: %w", err)
|
||||
}
|
||||
|
||||
abilityMaxLevels, err := utils.ReadJSON[CharacterBoardAbilityMaxLevelRow]("EntityMCharacterBoardAbilityMaxLevelTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board ability max level table: %w", err)
|
||||
}
|
||||
|
||||
targets, err := utils.ReadJSON[CharacterBoardEffectTargetRow]("EntityMCharacterBoardEffectTargetGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load character board effect target table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &CharacterBoardCatalog{
|
||||
PanelById: make(map[int32]CharacterBoardPanelRow, len(panels)),
|
||||
PanelsByBoardId: make(map[int32][]CharacterBoardPanelRow),
|
||||
ReleaseCostsByGroupId: make(map[int32][]CharacterBoardReleasePossessionRow),
|
||||
ReleaseEffectsByGroupId: make(map[int32][]CharacterBoardReleaseEffectRow),
|
||||
StatusUpById: make(map[int32]CharacterBoardStatusUpRow, len(statusUps)),
|
||||
AbilityById: make(map[int32]CharacterBoardAbilityRow, len(abilities)),
|
||||
AbilityMaxLevel: make(map[store.CharacterBoardAbilityKey]int32, len(abilityMaxLevels)),
|
||||
EffectTargetsByGroupId: make(map[int32][]CharacterBoardEffectTargetRow),
|
||||
BoardById: make(map[int32]CharacterBoardRow, len(boards)),
|
||||
}
|
||||
|
||||
for _, p := range panels {
|
||||
catalog.PanelById[p.CharacterBoardPanelId] = p
|
||||
catalog.PanelsByBoardId[p.CharacterBoardId] = append(catalog.PanelsByBoardId[p.CharacterBoardId], p)
|
||||
}
|
||||
for _, c := range costs {
|
||||
catalog.ReleaseCostsByGroupId[c.CharacterBoardPanelReleasePossessionGroupId] = append(
|
||||
catalog.ReleaseCostsByGroupId[c.CharacterBoardPanelReleasePossessionGroupId], c)
|
||||
}
|
||||
for _, e := range effects {
|
||||
catalog.ReleaseEffectsByGroupId[e.CharacterBoardPanelReleaseEffectGroupId] = append(
|
||||
catalog.ReleaseEffectsByGroupId[e.CharacterBoardPanelReleaseEffectGroupId], e)
|
||||
}
|
||||
for _, b := range boards {
|
||||
catalog.BoardById[b.CharacterBoardId] = b
|
||||
}
|
||||
for _, s := range statusUps {
|
||||
catalog.StatusUpById[s.CharacterBoardStatusUpId] = s
|
||||
}
|
||||
for _, a := range abilities {
|
||||
catalog.AbilityById[a.CharacterBoardAbilityId] = a
|
||||
}
|
||||
for _, m := range abilityMaxLevels {
|
||||
catalog.AbilityMaxLevel[store.CharacterBoardAbilityKey{
|
||||
CharacterId: m.CharacterId,
|
||||
AbilityId: m.AbilityId,
|
||||
}] = m.MaxLevel
|
||||
}
|
||||
for _, t := range targets {
|
||||
catalog.EffectTargetsByGroupId[t.CharacterBoardEffectTargetGroupId] = append(
|
||||
catalog.EffectTargetsByGroupId[t.CharacterBoardEffectTargetGroupId], t)
|
||||
}
|
||||
|
||||
return catalog, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type characterViewerField struct {
|
||||
CharacterViewerFieldId int32 `json:"CharacterViewerFieldId"`
|
||||
ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"`
|
||||
}
|
||||
|
||||
type characterViewerFieldEntry struct {
|
||||
FieldId int32
|
||||
RequiredQuestId int32
|
||||
}
|
||||
|
||||
type CharacterViewerCatalog struct {
|
||||
fields []characterViewerFieldEntry
|
||||
}
|
||||
|
||||
func (c *CharacterViewerCatalog) ReleasedFieldIds(user store.UserState) []int32 {
|
||||
var released []int32
|
||||
for _, f := range c.fields {
|
||||
if f.RequiredQuestId == 0 {
|
||||
released = append(released, f.FieldId)
|
||||
continue
|
||||
}
|
||||
q, ok := user.Quests[f.RequiredQuestId]
|
||||
if ok && q.QuestStateType == model.UserQuestStateTypeCleared {
|
||||
released = append(released, f.FieldId)
|
||||
}
|
||||
}
|
||||
return released
|
||||
}
|
||||
|
||||
func LoadCharacterViewerCatalog(resolver *ConditionResolver) *CharacterViewerCatalog {
|
||||
fields, err := utils.ReadJSON[characterViewerField]("EntityMCharacterViewerFieldTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load character viewer field table: %v", err)
|
||||
}
|
||||
|
||||
cat := &CharacterViewerCatalog{}
|
||||
for _, f := range fields {
|
||||
entry := characterViewerFieldEntry{FieldId: f.CharacterViewerFieldId}
|
||||
if qid, ok := resolver.RequiredQuestId(f.ReleaseEvaluateConditionId); ok {
|
||||
entry.RequiredQuestId = qid
|
||||
}
|
||||
cat.fields = append(cat.fields, entry)
|
||||
}
|
||||
|
||||
sort.Slice(cat.fields, func(i, j int) bool {
|
||||
return cat.fields[i].FieldId < cat.fields[j].FieldId
|
||||
})
|
||||
|
||||
log.Printf("character viewer catalog loaded: %d fields", len(cat.fields))
|
||||
return cat
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type companionRow struct {
|
||||
CompanionId int32 `json:"CompanionId"`
|
||||
CompanionCategoryType int32 `json:"CompanionCategoryType"`
|
||||
}
|
||||
|
||||
type companionCategoryRow struct {
|
||||
CompanionCategoryType int32 `json:"CompanionCategoryType"`
|
||||
EnhancementCostNumericalFunctionId int32 `json:"EnhancementCostNumericalFunctionId"`
|
||||
}
|
||||
|
||||
type companionEnhancementMaterialRow struct {
|
||||
CompanionCategoryType int32 `json:"CompanionCategoryType"`
|
||||
Level int32 `json:"Level"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type CompanionLevelKey struct {
|
||||
CategoryType int32
|
||||
Level int32
|
||||
}
|
||||
|
||||
type CompanionMaterialCost struct {
|
||||
MaterialId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
type CompanionCatalog struct {
|
||||
CompanionById map[int32]companionRow
|
||||
GoldCostByCategory map[int32]NumericalFunc
|
||||
MaterialsByKey map[CompanionLevelKey]CompanionMaterialCost
|
||||
}
|
||||
|
||||
func LoadCompanionCatalog() (*CompanionCatalog, error) {
|
||||
companions, err := utils.ReadJSON[companionRow]("EntityMCompanionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load companion table: %w", err)
|
||||
}
|
||||
|
||||
categories, err := utils.ReadJSON[companionCategoryRow]("EntityMCompanionCategoryTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load companion category table: %w", err)
|
||||
}
|
||||
|
||||
materials, err := utils.ReadJSON[companionEnhancementMaterialRow]("EntityMCompanionEnhancementMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load companion enhancement material table: %w", err)
|
||||
}
|
||||
|
||||
funcResolver, err := LoadFunctionResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load function resolver: %w", err)
|
||||
}
|
||||
|
||||
companionById := make(map[int32]companionRow, len(companions))
|
||||
for _, c := range companions {
|
||||
companionById[c.CompanionId] = c
|
||||
}
|
||||
|
||||
goldCostByCategory := make(map[int32]NumericalFunc, len(categories))
|
||||
for _, cat := range categories {
|
||||
if f, ok := funcResolver.Resolve(cat.EnhancementCostNumericalFunctionId); ok {
|
||||
goldCostByCategory[cat.CompanionCategoryType] = f
|
||||
}
|
||||
}
|
||||
|
||||
materialsByKey := make(map[CompanionLevelKey]CompanionMaterialCost, len(materials))
|
||||
for _, m := range materials {
|
||||
key := CompanionLevelKey{CategoryType: m.CompanionCategoryType, Level: m.Level}
|
||||
materialsByKey[key] = CompanionMaterialCost{MaterialId: m.MaterialId, Count: m.Count}
|
||||
}
|
||||
|
||||
return &CompanionCatalog{
|
||||
CompanionById: companionById,
|
||||
GoldCostByCategory: goldCostByCategory,
|
||||
MaterialsByKey: materialsByKey,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type evaluateCondition struct {
|
||||
EvaluateConditionId int32 `json:"EvaluateConditionId"`
|
||||
EvaluateConditionFunctionType model.EvaluateConditionFunctionType `json:"EvaluateConditionFunctionType"`
|
||||
EvaluateConditionEvaluateType model.EvaluateConditionEvaluateType `json:"EvaluateConditionEvaluateType"`
|
||||
EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"`
|
||||
}
|
||||
|
||||
type evaluateConditionValueGroup struct {
|
||||
EvaluateConditionValueGroupId int32 `json:"EvaluateConditionValueGroupId"`
|
||||
GroupIndex int32 `json:"GroupIndex"`
|
||||
Value int64 `json:"Value"`
|
||||
}
|
||||
|
||||
const defaultGroupIndex = 1
|
||||
|
||||
type ConditionResolver struct {
|
||||
requiredQuestByCondId map[int32]int32
|
||||
}
|
||||
|
||||
func LoadConditionResolver() (*ConditionResolver, error) {
|
||||
conditions, err := utils.ReadJSON[evaluateCondition]("EntityMEvaluateConditionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load evaluate condition table: %w", err)
|
||||
}
|
||||
valueGroups, err := utils.ReadJSON[evaluateConditionValueGroup]("EntityMEvaluateConditionValueGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load evaluate condition value group table: %w", err)
|
||||
}
|
||||
|
||||
condById := make(map[int32]evaluateCondition, len(conditions))
|
||||
for _, c := range conditions {
|
||||
condById[c.EvaluateConditionId] = c
|
||||
}
|
||||
|
||||
type vgKey struct {
|
||||
GroupId int32
|
||||
GroupIndex int32
|
||||
}
|
||||
vgByKey := make(map[vgKey]int64, len(valueGroups))
|
||||
for _, vg := range valueGroups {
|
||||
vgByKey[vgKey{vg.EvaluateConditionValueGroupId, vg.GroupIndex}] = vg.Value
|
||||
}
|
||||
|
||||
resolved := make(map[int32]int32)
|
||||
for _, c := range conditions {
|
||||
if c.EvaluateConditionFunctionType == model.EvaluateConditionFunctionTypeQuestClear &&
|
||||
c.EvaluateConditionEvaluateType == model.EvaluateConditionEvaluateTypeIdContain {
|
||||
if questId, ok := vgByKey[vgKey{c.EvaluateConditionValueGroupId, defaultGroupIndex}]; ok {
|
||||
resolved[c.EvaluateConditionId] = int32(questId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ConditionResolver{requiredQuestByCondId: resolved}, nil
|
||||
}
|
||||
|
||||
func (r *ConditionResolver) RequiredQuestId(conditionId int32) (int32, bool) {
|
||||
qid, ok := r.requiredQuestByCondId[conditionId]
|
||||
return qid, ok
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type configRow struct {
|
||||
ConfigKey string `json:"ConfigKey"`
|
||||
Value string `json:"Value"`
|
||||
}
|
||||
|
||||
type GameConfig struct {
|
||||
ConsumableItemIdForGold int32
|
||||
ConsumableItemIdForMedal int32
|
||||
ConsumableItemIdForRareMedal int32
|
||||
ConsumableItemIdForArenaCoin int32
|
||||
ConsumableItemIdForExploreTicket int32
|
||||
ConsumableItemIdForMomPoint int32
|
||||
ConsumableItemIdForPremiumGachaTicket int32
|
||||
ConsumableItemIdForQuestSkipTicket int32
|
||||
|
||||
CharacterRebirthAvailableCount int32
|
||||
CharacterRebirthConsumeGold int32
|
||||
|
||||
CostumeAwakenAvailableCount int32
|
||||
CostumeLimitBreakAvailableCount int32
|
||||
|
||||
MaterialSameWeaponExpCoefficientPermil int32
|
||||
|
||||
UserStaminaRecoverySecond int32
|
||||
RewardGachaDailyMaxCount int32
|
||||
QuestSkipMaxCountAtOnce int32
|
||||
|
||||
WeaponLimitBreakAvailableCount int32
|
||||
}
|
||||
|
||||
func LoadGameConfig() (*GameConfig, error) {
|
||||
rows, err := utils.ReadJSON[configRow]("EntityMConfigTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load config table: %w", err)
|
||||
}
|
||||
|
||||
kv := make(map[string]string, len(rows))
|
||||
for _, r := range rows {
|
||||
kv[r.ConfigKey] = r.Value
|
||||
}
|
||||
|
||||
cfg := &GameConfig{}
|
||||
|
||||
cfg.ConsumableItemIdForGold = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_GOLD")
|
||||
cfg.ConsumableItemIdForMedal = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_MEDAL")
|
||||
cfg.ConsumableItemIdForRareMedal = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_RARE_MEDAL")
|
||||
cfg.ConsumableItemIdForArenaCoin = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_ARENA_COIN")
|
||||
cfg.ConsumableItemIdForExploreTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_EXPLORE_TICKET")
|
||||
cfg.ConsumableItemIdForMomPoint = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_MOM_POINT")
|
||||
cfg.ConsumableItemIdForPremiumGachaTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_PREMIUM_GACHA_TICKET")
|
||||
cfg.ConsumableItemIdForQuestSkipTicket = parseInt32(kv, "CONSUMABLE_ITEM_ID_FOR_QUEST_SKIP_TICKET")
|
||||
|
||||
cfg.CharacterRebirthAvailableCount = parseInt32(kv, "CHARACTER_REBIRTH_AVAILABLE_COUNT")
|
||||
cfg.CharacterRebirthConsumeGold = parseInt32(kv, "CHARACTER_REBIRTH_CONSUME_GOLD")
|
||||
|
||||
cfg.CostumeAwakenAvailableCount = parseInt32(kv, "COSTUME_AWAKEN_AVAILABLE_COUNT")
|
||||
cfg.CostumeLimitBreakAvailableCount = parseInt32(kv, "COSTUME_LIMIT_BREAK_AVAILABLE_COUNT")
|
||||
|
||||
cfg.MaterialSameWeaponExpCoefficientPermil = parseInt32(kv, "MATERIAL_SAME_WEAPON_EXP_COEFFICIENT_PERMIL")
|
||||
|
||||
cfg.UserStaminaRecoverySecond = parseInt32(kv, "USER_STAMINA_RECOVERY_SECOND")
|
||||
cfg.RewardGachaDailyMaxCount = parseInt32(kv, "REWARD_GACHA_DAILY_MAX_COUNT")
|
||||
cfg.QuestSkipMaxCountAtOnce = parseInt32(kv, "QUEST_SKIP_MAX_COUNT_AT_ONCE")
|
||||
|
||||
cfg.WeaponLimitBreakAvailableCount = parseInt32(kv, "WEAPON_LIMIT_BREAK_AVAILABLE_COUNT")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseInt32(kv map[string]string, key string) int32 {
|
||||
s, ok := kv[key]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
v, err := strconv.ParseInt(s, 10, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int32(v)
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type CostumeMasterRow struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
SkillfulWeaponType int32 `json:"SkillfulWeaponType"`
|
||||
RarityType int32 `json:"RarityType"`
|
||||
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
|
||||
CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"`
|
||||
}
|
||||
|
||||
type costumeRarityRow struct {
|
||||
RarityType int32 `json:"RarityType"`
|
||||
CostumeLimitBreakMaterialRarityGroupId int32 `json:"CostumeLimitBreakMaterialRarityGroupId"`
|
||||
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
|
||||
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
|
||||
LimitBreakCostNumericalFunctionId int32 `json:"LimitBreakCostNumericalFunctionId"`
|
||||
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
|
||||
ActiveSkillMaxLevelNumericalFunctionId int32 `json:"ActiveSkillMaxLevelNumericalFunctionId"`
|
||||
ActiveSkillEnhancementCostNumericalFunctionId int32 `json:"ActiveSkillEnhancementCostNumericalFunctionId"`
|
||||
}
|
||||
|
||||
type CostumeAwakenRow struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"`
|
||||
CostumeAwakenStepMaterialGroupId int32 `json:"CostumeAwakenStepMaterialGroupId"`
|
||||
CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"`
|
||||
}
|
||||
|
||||
type costumeAwakenPriceRow struct {
|
||||
CostumeAwakenPriceGroupId int32 `json:"CostumeAwakenPriceGroupId"`
|
||||
AwakenStepLowerLimit int32 `json:"AwakenStepLowerLimit"`
|
||||
Gold int32 `json:"Gold"`
|
||||
}
|
||||
|
||||
type CostumeAwakenEffectRow struct {
|
||||
CostumeAwakenEffectGroupId int32 `json:"CostumeAwakenEffectGroupId"`
|
||||
AwakenStep int32 `json:"AwakenStep"`
|
||||
CostumeAwakenEffectType int32 `json:"CostumeAwakenEffectType"`
|
||||
CostumeAwakenEffectId int32 `json:"CostumeAwakenEffectId"`
|
||||
}
|
||||
|
||||
type CostumeAwakenStatusUpRow struct {
|
||||
CostumeAwakenStatusUpGroupId int32 `json:"CostumeAwakenStatusUpGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
StatusKindType int32 `json:"StatusKindType"`
|
||||
StatusCalculationType int32 `json:"StatusCalculationType"`
|
||||
EffectValue int32 `json:"EffectValue"`
|
||||
}
|
||||
|
||||
type CostumeAwakenItemAcquireRow struct {
|
||||
CostumeAwakenItemAcquireId int32 `json:"CostumeAwakenItemAcquireId"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type CostumeActiveSkillGroupRow struct {
|
||||
CostumeActiveSkillGroupId int32 `json:"CostumeActiveSkillGroupId"`
|
||||
CostumeLimitBreakCountLowerLimit int32 `json:"CostumeLimitBreakCountLowerLimit"`
|
||||
CostumeActiveSkillId int32 `json:"CostumeActiveSkillId"`
|
||||
CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"`
|
||||
}
|
||||
|
||||
type CostumeActiveSkillEnhanceMaterialRow struct {
|
||||
CostumeActiveSkillEnhancementMaterialId int32 `json:"CostumeActiveSkillEnhancementMaterialId"`
|
||||
SkillLevel int32 `json:"SkillLevel"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type CostumeCatalog struct {
|
||||
Costumes map[int32]CostumeMasterRow
|
||||
Materials map[int32]MaterialRow
|
||||
ExpByRarity map[int32][]int32
|
||||
EnhanceCostByRarity map[int32]NumericalFunc
|
||||
MaxLevelByRarity map[int32]NumericalFunc
|
||||
LimitBreakCostByRarity map[int32]NumericalFunc
|
||||
|
||||
AwakenByCostumeId map[int32]CostumeAwakenRow
|
||||
AwakenPriceByGroup map[int32]int32
|
||||
AwakenEffectsByGroupAndStep map[int32]map[int32]CostumeAwakenEffectRow
|
||||
AwakenStatusUpByGroup map[int32][]CostumeAwakenStatusUpRow
|
||||
AwakenItemAcquireById map[int32]CostumeAwakenItemAcquireRow
|
||||
|
||||
ActiveSkillGroupsByGroupId map[int32][]CostumeActiveSkillGroupRow // sorted by CostumeLimitBreakCountLowerLimit desc
|
||||
ActiveSkillEnhanceMats map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
|
||||
ActiveSkillMaxLevelByRarity map[int32]NumericalFunc
|
||||
ActiveSkillCostByRarity map[int32]NumericalFunc
|
||||
}
|
||||
|
||||
func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
|
||||
costumes, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume table: %w", err)
|
||||
}
|
||||
|
||||
rarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume rarity table: %w", err)
|
||||
}
|
||||
|
||||
paramMapRows, err := LoadParameterMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
funcResolver, err := LoadFunctionResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load function resolver: %w", err)
|
||||
}
|
||||
|
||||
awakenRows, err := utils.ReadJSON[CostumeAwakenRow]("EntityMCostumeAwakenTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume awaken table: %w", err)
|
||||
}
|
||||
awakenPriceRows, err := utils.ReadJSON[costumeAwakenPriceRow]("EntityMCostumeAwakenPriceGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume awaken price table: %w", err)
|
||||
}
|
||||
awakenEffectRows, err := utils.ReadJSON[CostumeAwakenEffectRow]("EntityMCostumeAwakenEffectGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume awaken effect table: %w", err)
|
||||
}
|
||||
awakenStatusUpRows, err := utils.ReadJSON[CostumeAwakenStatusUpRow]("EntityMCostumeAwakenStatusUpGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume awaken status up table: %w", err)
|
||||
}
|
||||
awakenItemAcquireRows, err := utils.ReadJSON[CostumeAwakenItemAcquireRow]("EntityMCostumeAwakenItemAcquireTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume awaken item acquire table: %w", err)
|
||||
}
|
||||
|
||||
activeSkillGroupRows, err := utils.ReadJSON[CostumeActiveSkillGroupRow]("EntityMCostumeActiveSkillGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume active skill group table: %w", err)
|
||||
}
|
||||
activeSkillMatRows, err := utils.ReadJSON[CostumeActiveSkillEnhanceMaterialRow]("EntityMCostumeActiveSkillEnhancementMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume active skill enhancement material table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &CostumeCatalog{
|
||||
Costumes: make(map[int32]CostumeMasterRow, len(costumes)),
|
||||
Materials: matCatalog.ByType[model.MaterialTypeCostumeEnhancement],
|
||||
ExpByRarity: make(map[int32][]int32, len(rarities)),
|
||||
EnhanceCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
|
||||
MaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
|
||||
LimitBreakCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
|
||||
|
||||
AwakenByCostumeId: make(map[int32]CostumeAwakenRow, len(awakenRows)),
|
||||
AwakenPriceByGroup: make(map[int32]int32, len(awakenPriceRows)),
|
||||
AwakenEffectsByGroupAndStep: make(map[int32]map[int32]CostumeAwakenEffectRow),
|
||||
AwakenStatusUpByGroup: make(map[int32][]CostumeAwakenStatusUpRow),
|
||||
AwakenItemAcquireById: make(map[int32]CostumeAwakenItemAcquireRow, len(awakenItemAcquireRows)),
|
||||
|
||||
ActiveSkillGroupsByGroupId: make(map[int32][]CostumeActiveSkillGroupRow),
|
||||
ActiveSkillEnhanceMats: make(map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow),
|
||||
ActiveSkillMaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
|
||||
ActiveSkillCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
|
||||
}
|
||||
|
||||
for _, row := range costumes {
|
||||
catalog.Costumes[row.CostumeId] = row
|
||||
}
|
||||
|
||||
for _, r := range rarities {
|
||||
if _, ok := catalog.ExpByRarity[r.RarityType]; !ok {
|
||||
catalog.ExpByRarity[r.RarityType] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
|
||||
}
|
||||
if _, ok := catalog.EnhanceCostByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
|
||||
catalog.EnhanceCostByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.MaxLevelByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
|
||||
catalog.MaxLevelByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.LimitBreakCostByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.LimitBreakCostNumericalFunctionId); found {
|
||||
catalog.LimitBreakCostByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.ActiveSkillMaxLevelByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.ActiveSkillMaxLevelNumericalFunctionId); found {
|
||||
catalog.ActiveSkillMaxLevelByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.ActiveSkillCostByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.ActiveSkillEnhancementCostNumericalFunctionId); found {
|
||||
catalog.ActiveSkillCostByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range awakenRows {
|
||||
catalog.AwakenByCostumeId[row.CostumeId] = row
|
||||
}
|
||||
for _, row := range awakenPriceRows {
|
||||
catalog.AwakenPriceByGroup[row.CostumeAwakenPriceGroupId] = row.Gold
|
||||
}
|
||||
for _, row := range awakenEffectRows {
|
||||
m, ok := catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId]
|
||||
if !ok {
|
||||
m = make(map[int32]CostumeAwakenEffectRow)
|
||||
catalog.AwakenEffectsByGroupAndStep[row.CostumeAwakenEffectGroupId] = m
|
||||
}
|
||||
m[row.AwakenStep] = row
|
||||
}
|
||||
for _, row := range awakenStatusUpRows {
|
||||
catalog.AwakenStatusUpByGroup[row.CostumeAwakenStatusUpGroupId] = append(
|
||||
catalog.AwakenStatusUpByGroup[row.CostumeAwakenStatusUpGroupId], row)
|
||||
}
|
||||
for _, row := range awakenItemAcquireRows {
|
||||
catalog.AwakenItemAcquireById[row.CostumeAwakenItemAcquireId] = row
|
||||
}
|
||||
|
||||
for _, row := range activeSkillGroupRows {
|
||||
gid := row.CostumeActiveSkillGroupId
|
||||
catalog.ActiveSkillGroupsByGroupId[gid] = append(catalog.ActiveSkillGroupsByGroupId[gid], row)
|
||||
}
|
||||
for gid, rows := range catalog.ActiveSkillGroupsByGroupId {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].CostumeLimitBreakCountLowerLimit > rows[j].CostumeLimitBreakCountLowerLimit
|
||||
})
|
||||
catalog.ActiveSkillGroupsByGroupId[gid] = rows
|
||||
}
|
||||
|
||||
for _, row := range activeSkillMatRows {
|
||||
key := [2]int32{row.CostumeActiveSkillEnhancementMaterialId, row.SkillLevel}
|
||||
catalog.ActiveSkillEnhanceMats[key] = append(catalog.ActiveSkillEnhanceMats[key], row)
|
||||
}
|
||||
|
||||
return catalog, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type costumeDupRow struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
func LoadDupExchange() (map[int32][]model.DupExchangeEntry, error) {
|
||||
result := make(map[int32][]model.DupExchangeEntry)
|
||||
|
||||
costumeRows, err := utils.ReadJSON[costumeDupRow]("EntityMCostumeDuplicationExchangePossessionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range costumeRows {
|
||||
result[r.CostumeId] = append(result[r.CostumeId], model.DupExchangeEntry{
|
||||
PossessionType: r.PossessionType,
|
||||
PossessionId: r.PossessionId,
|
||||
Count: r.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type lbMaterialRow struct {
|
||||
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
}
|
||||
|
||||
type costumeLBRef struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
CostumeLimitBreakMaterialGroupId int32 `json:"CostumeLimitBreakMaterialGroupId"`
|
||||
}
|
||||
|
||||
const dupExchangeFallbackCount int32 = 10
|
||||
|
||||
func EnrichDupExchange(dupMap map[int32][]model.DupExchangeEntry, pool *GachaCatalog) (int, error) {
|
||||
lbRows, err := utils.ReadJSON[lbMaterialRow]("EntityMCostumeLimitBreakMaterialGroupTable.json")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groupToMaterial := make(map[int32]int32, len(lbRows))
|
||||
for _, r := range lbRows {
|
||||
groupToMaterial[r.CostumeLimitBreakMaterialGroupId] = r.MaterialId
|
||||
}
|
||||
|
||||
costumeRows, err := utils.ReadJSON[costumeLBRef]("EntityMCostumeTable.json")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
costumeLBGroup := make(map[int32]int32, len(costumeRows))
|
||||
for _, r := range costumeRows {
|
||||
costumeLBGroup[r.CostumeId] = r.CostumeLimitBreakMaterialGroupId
|
||||
}
|
||||
|
||||
added := 0
|
||||
for costumeId := range pool.CostumeById {
|
||||
if _, exists := dupMap[costumeId]; exists {
|
||||
continue
|
||||
}
|
||||
matId := groupToMaterial[costumeLBGroup[costumeId]]
|
||||
if matId == 0 {
|
||||
continue
|
||||
}
|
||||
dupMap[costumeId] = []model.DupExchangeEntry{{
|
||||
PossessionType: int32(model.PossessionTypeMaterial),
|
||||
PossessionId: matId,
|
||||
Count: dupExchangeFallbackCount,
|
||||
}}
|
||||
added++
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type ExploreRow struct {
|
||||
ExploreId int32 `json:"ExploreId"`
|
||||
ConsumeItemCount int32 `json:"ConsumeItemCount"`
|
||||
RewardLotteryCount int32 `json:"RewardLotteryCount"`
|
||||
}
|
||||
|
||||
type ExploreGradeScoreRow struct {
|
||||
ExploreId int32 `json:"ExploreId"`
|
||||
NecessaryScore int32 `json:"NecessaryScore"`
|
||||
ExploreGradeId int32 `json:"ExploreGradeId"`
|
||||
}
|
||||
|
||||
type ExploreGradeAssetRow struct {
|
||||
ExploreGradeId int32 `json:"ExploreGradeId"`
|
||||
AssetGradeIconId int32 `json:"AssetGradeIconId"`
|
||||
}
|
||||
|
||||
type ExploreCatalog struct {
|
||||
Explores map[int32]ExploreRow
|
||||
GradeScores map[int32][]ExploreGradeScoreRow // keyed by ExploreId, sorted desc by NecessaryScore
|
||||
GradeAssets map[int32]int32 // gradeId -> assetGradeIconId
|
||||
}
|
||||
|
||||
func LoadExploreCatalog() (*ExploreCatalog, error) {
|
||||
explores, err := utils.ReadJSON[ExploreRow]("EntityMExploreTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load explore table: %w", err)
|
||||
}
|
||||
|
||||
gradeScores, err := utils.ReadJSON[ExploreGradeScoreRow]("EntityMExploreGradeScoreTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load explore grade score table: %w", err)
|
||||
}
|
||||
|
||||
gradeAssets, err := utils.ReadJSON[ExploreGradeAssetRow]("EntityMExploreGradeAssetTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load explore grade asset table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &ExploreCatalog{
|
||||
Explores: make(map[int32]ExploreRow, len(explores)),
|
||||
GradeScores: make(map[int32][]ExploreGradeScoreRow),
|
||||
GradeAssets: make(map[int32]int32, len(gradeAssets)),
|
||||
}
|
||||
|
||||
for _, e := range explores {
|
||||
catalog.Explores[e.ExploreId] = e
|
||||
}
|
||||
|
||||
for _, gs := range gradeScores {
|
||||
catalog.GradeScores[gs.ExploreId] = append(catalog.GradeScores[gs.ExploreId], gs)
|
||||
}
|
||||
for eid := range catalog.GradeScores {
|
||||
rows := catalog.GradeScores[eid]
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].NecessaryScore > rows[j].NecessaryScore
|
||||
})
|
||||
catalog.GradeScores[eid] = rows
|
||||
}
|
||||
|
||||
for _, ga := range gradeAssets {
|
||||
catalog.GradeAssets[ga.ExploreGradeId] = ga.AssetGradeIconId
|
||||
}
|
||||
|
||||
return catalog, nil
|
||||
}
|
||||
|
||||
// GradeForScore returns the AssetGradeIconId for the given explore and score.
|
||||
// Returns 0 if no matching grade is found.
|
||||
func (c *ExploreCatalog) GradeForScore(exploreId, score int32) int32 {
|
||||
rows, ok := c.GradeScores[exploreId]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
for _, r := range rows {
|
||||
if score >= r.NecessaryScore {
|
||||
return c.GradeAssets[r.ExploreGradeId]
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type gachaMedalRow struct {
|
||||
GachaMedalId int32 `json:"GachaMedalId"`
|
||||
ShopTransitionGachaId int32 `json:"ShopTransitionGachaId"`
|
||||
ConsumableItemId int32 `json:"ConsumableItemId"`
|
||||
AutoConvertDatetime int64 `json:"AutoConvertDatetime"`
|
||||
ConversionRate int32 `json:"ConversionRate"`
|
||||
}
|
||||
|
||||
type momBannerRow struct {
|
||||
MomBannerId int32 `json:"MomBannerId"`
|
||||
SortOrderDesc int32 `json:"SortOrderDesc"`
|
||||
DestinationDomainType int32 `json:"DestinationDomainType"`
|
||||
DestinationDomainId int32 `json:"DestinationDomainId"`
|
||||
BannerAssetName string `json:"BannerAssetName"`
|
||||
StartDatetime int64 `json:"StartDatetime"`
|
||||
EndDatetime int64 `json:"EndDatetime"`
|
||||
}
|
||||
|
||||
type GachaMedalInfo struct {
|
||||
GachaMedalId int32
|
||||
ConsumableItemId int32
|
||||
AutoConvertDatetime int64
|
||||
ConversionRate int32
|
||||
}
|
||||
|
||||
const chapterGachaIdBase int32 = 200000
|
||||
|
||||
func LoadGachaCatalog() ([]store.GachaCatalogEntry, map[int32]GachaMedalInfo, error) {
|
||||
medals, err := utils.ReadJSON[gachaMedalRow]("EntityMGachaMedalTable.json")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load gacha medal table: %w", err)
|
||||
}
|
||||
banners, err := utils.ReadJSON[momBannerRow]("EntityMMomBannerTable.json")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load mom banner table: %w", err)
|
||||
}
|
||||
|
||||
gachaToMedal := make(map[int32]gachaMedalRow)
|
||||
medalInfoByGacha := make(map[int32]GachaMedalInfo)
|
||||
for _, m := range medals {
|
||||
gachaToMedal[m.ShopTransitionGachaId] = m
|
||||
medalInfoByGacha[m.ShopTransitionGachaId] = GachaMedalInfo{
|
||||
GachaMedalId: m.GachaMedalId,
|
||||
ConsumableItemId: m.ConsumableItemId,
|
||||
AutoConvertDatetime: m.AutoConvertDatetime,
|
||||
ConversionRate: m.ConversionRate,
|
||||
}
|
||||
}
|
||||
|
||||
stepupSteps := make(map[int32][]momBannerRow)
|
||||
var entries []store.GachaCatalogEntry
|
||||
|
||||
for _, b := range banners {
|
||||
if b.DestinationDomainType != model.MomBannerDomainGacha {
|
||||
continue
|
||||
}
|
||||
gachaId := b.DestinationDomainId
|
||||
|
||||
if strings.HasPrefix(b.BannerAssetName, model.BannerPrefixStepUp) {
|
||||
if _, hasMedal := gachaToMedal[gachaId]; !hasMedal {
|
||||
continue
|
||||
}
|
||||
groupId := gachaId / model.StepUpGroupDivisor
|
||||
stepupSteps[groupId] = append(stepupSteps[groupId], b)
|
||||
continue
|
||||
}
|
||||
|
||||
labelType := model.GachaLabelPremium
|
||||
modeType := model.GachaModeBasic
|
||||
decoration := model.GachaDecorationNormal
|
||||
|
||||
isChapter := strings.HasPrefix(b.BannerAssetName, model.BannerPrefixCommon)
|
||||
|
||||
if strings.HasPrefix(b.BannerAssetName, model.BannerPrefixLimited) {
|
||||
decoration = model.GachaDecorationFestival
|
||||
}
|
||||
if isChapter {
|
||||
labelType = model.GachaLabelChapter
|
||||
modeType = model.GachaModeBox
|
||||
}
|
||||
|
||||
medal, hasMedal := gachaToMedal[gachaId]
|
||||
if !hasMedal && !isChapter {
|
||||
continue
|
||||
}
|
||||
var medalId int32
|
||||
var medalConsumableId int32
|
||||
var ceilingCount int32
|
||||
if hasMedal {
|
||||
medalId = medal.GachaMedalId
|
||||
medalConsumableId = medal.ConsumableItemId
|
||||
ceilingCount = model.PityCeilingCount
|
||||
}
|
||||
|
||||
var pricePhases []store.GachaPricePhaseEntry
|
||||
if isChapter {
|
||||
pricePhases = buildChapterPricePhases(gachaId)
|
||||
} else {
|
||||
pricePhases = buildPremiumBasicPricePhases(gachaId)
|
||||
}
|
||||
|
||||
relMainQuest := int32(0)
|
||||
if isChapter {
|
||||
relMainQuest = gachaId - chapterGachaIdBase
|
||||
}
|
||||
|
||||
var descriptionTextId int32
|
||||
if isChapter {
|
||||
descriptionTextId = gachaId
|
||||
}
|
||||
|
||||
entries = append(entries, store.GachaCatalogEntry{
|
||||
GachaId: gachaId,
|
||||
GachaLabelType: labelType,
|
||||
GachaModeType: modeType,
|
||||
GachaAutoResetType: model.GachaAutoResetNone,
|
||||
IsUserGachaUnlock: true,
|
||||
StartDatetime: b.StartDatetime,
|
||||
EndDatetime: b.EndDatetime,
|
||||
RelatedMainQuestChapterId: relMainQuest,
|
||||
GachaMedalId: medalId,
|
||||
MedalConsumableItemId: medalConsumableId,
|
||||
GachaDecorationType: decoration,
|
||||
SortOrder: b.SortOrderDesc,
|
||||
BannerAssetName: b.BannerAssetName,
|
||||
GroupId: gachaId,
|
||||
CeilingCount: ceilingCount,
|
||||
PricePhases: pricePhases,
|
||||
DescriptionTextId: descriptionTextId,
|
||||
})
|
||||
}
|
||||
|
||||
for groupId, steps := range stepupSteps {
|
||||
first := steps[0]
|
||||
gachaId := groupId
|
||||
|
||||
medal := gachaToMedal[first.DestinationDomainId]
|
||||
medalId := medal.GachaMedalId
|
||||
medalConsumableId := medal.ConsumableItemId
|
||||
|
||||
pricePhases := buildStepUpPricePhases(gachaId, len(steps))
|
||||
|
||||
var maxStep int32
|
||||
for _, p := range pricePhases {
|
||||
if p.StepNumber > maxStep {
|
||||
maxStep = p.StepNumber
|
||||
}
|
||||
}
|
||||
|
||||
entries = append(entries, store.GachaCatalogEntry{
|
||||
GachaId: gachaId,
|
||||
GachaLabelType: model.GachaLabelPremium,
|
||||
GachaModeType: model.GachaModeStepup,
|
||||
GachaAutoResetType: model.GachaAutoResetNone,
|
||||
IsUserGachaUnlock: true,
|
||||
StartDatetime: first.StartDatetime,
|
||||
EndDatetime: first.EndDatetime,
|
||||
GachaMedalId: medalId,
|
||||
MedalConsumableItemId: medalConsumableId,
|
||||
GachaDecorationType: model.GachaDecorationFestival,
|
||||
SortOrder: first.SortOrderDesc,
|
||||
BannerAssetName: first.BannerAssetName,
|
||||
GroupId: groupId,
|
||||
CeilingCount: model.PityCeilingCount,
|
||||
PricePhases: pricePhases,
|
||||
MaxStepNumber: maxStep,
|
||||
})
|
||||
}
|
||||
|
||||
return entries, medalInfoByGacha, nil
|
||||
}
|
||||
|
||||
const chapterPromoMaxItems = 4
|
||||
const maxSlideFeatured = 13
|
||||
|
||||
func EnrichCatalogPromotions(entries []store.GachaCatalogEntry, pool *GachaCatalog) {
|
||||
for i := range entries {
|
||||
if entries[i].GachaLabelType == model.GachaLabelChapter {
|
||||
entries[i].PromotionItems = buildChapterPromotionItems(pool.Materials)
|
||||
continue
|
||||
}
|
||||
|
||||
featured := pool.FeaturedByGacha[entries[i].GachaId]
|
||||
|
||||
maxRarity := int32(0)
|
||||
for _, c := range featured.Costumes {
|
||||
if c.RarityType > maxRarity {
|
||||
maxRarity = c.RarityType
|
||||
}
|
||||
}
|
||||
for _, w := range featured.Weapons {
|
||||
if w.RarityType > maxRarity {
|
||||
maxRarity = w.RarityType
|
||||
}
|
||||
}
|
||||
|
||||
var topCostumes []GachaPoolItem
|
||||
for _, c := range featured.Costumes {
|
||||
if c.RarityType == maxRarity {
|
||||
topCostumes = append(topCostumes, c)
|
||||
}
|
||||
}
|
||||
var topWeapons []GachaPoolItem
|
||||
for _, w := range featured.Weapons {
|
||||
if w.RarityType == maxRarity {
|
||||
topWeapons = append(topWeapons, w)
|
||||
}
|
||||
}
|
||||
|
||||
if len(topCostumes)+len(topWeapons) > maxSlideFeatured {
|
||||
topCostumes = topCostumes[:min(3, len(topCostumes))]
|
||||
topWeapons = topWeapons[:min(2, len(topWeapons))]
|
||||
}
|
||||
|
||||
var items []store.GachaPromotionItem
|
||||
if entries[i].GachaModeType == model.GachaModeStepup && len(topCostumes) > 0 {
|
||||
items = append(items, toPromoItemWithBonus(topCostumes[0], pool))
|
||||
wid := pool.CostumeWeaponMap[topCostumes[0].PossessionId]
|
||||
items = append(items, toPromoItem(pool.WeaponById[wid]))
|
||||
} else {
|
||||
for _, c := range topCostumes {
|
||||
items = append(items, toPromoItemWithBonus(c, pool))
|
||||
}
|
||||
for _, w := range topWeapons {
|
||||
items = append(items, toPromoItemWithBonus(w, pool))
|
||||
}
|
||||
}
|
||||
|
||||
entries[i].PromotionItems = items
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
if entries[i].SortOrder != entries[j].SortOrder {
|
||||
return entries[i].SortOrder < entries[j].SortOrder
|
||||
}
|
||||
return entries[i].GachaId < entries[j].GachaId
|
||||
})
|
||||
}
|
||||
|
||||
func toPromoItem(item GachaPoolItem) store.GachaPromotionItem {
|
||||
return store.GachaPromotionItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
IsTarget: true,
|
||||
}
|
||||
}
|
||||
|
||||
func toPromoItemWithBonus(item GachaPoolItem, pool *GachaCatalog) store.GachaPromotionItem {
|
||||
pi := store.GachaPromotionItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
IsTarget: true,
|
||||
}
|
||||
if item.PossessionType == int32(model.PossessionTypeCostume) {
|
||||
pi.BonusPossessionType = int32(model.PossessionTypeWeapon)
|
||||
pi.BonusPossessionId = pool.CostumeWeaponMap[item.PossessionId]
|
||||
}
|
||||
return pi
|
||||
}
|
||||
|
||||
func buildChapterPromotionItems(materials []GachaPoolItem) []store.GachaPromotionItem {
|
||||
limit := min(chapterPromoMaxItems, len(materials))
|
||||
items := make([]store.GachaPromotionItem, 0, limit)
|
||||
for _, m := range materials[:limit] {
|
||||
items = append(items, toPromoItem(m))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func buildPremiumBasicPricePhases(gachaId int32) []store.GachaPricePhaseEntry {
|
||||
return []store.GachaPricePhaseEntry{
|
||||
{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + 1,
|
||||
PriceType: model.PriceTypeGem,
|
||||
Price: model.PremiumSinglePullPrice,
|
||||
RegularPrice: model.PremiumSinglePullPrice,
|
||||
DrawCount: 1,
|
||||
},
|
||||
{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + 2,
|
||||
PriceType: model.PriceTypeGem,
|
||||
Price: model.PremiumMultiPullPrice,
|
||||
RegularPrice: model.PremiumMultiPullPrice,
|
||||
DrawCount: model.PremiumMultiPullCount,
|
||||
FixedRarityMin: model.RaritySRare,
|
||||
FixedCount: 1,
|
||||
},
|
||||
{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + 3,
|
||||
PriceType: model.PriceTypeConsumableItem,
|
||||
PriceId: model.ConsumableIdPremiumTicket,
|
||||
Price: 1,
|
||||
RegularPrice: 1,
|
||||
DrawCount: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildStepUpPricePhases(gachaId int32, totalSteps int) []store.GachaPricePhaseEntry {
|
||||
stepCosts := []int32{model.StepUpStep1Cost, model.StepUpFreeCost, model.StepUpStep3Cost, model.StepUpFreeCost, model.StepUpStep5Cost}
|
||||
stepCosts = stepCosts[:min(totalSteps, len(stepCosts))]
|
||||
|
||||
var phases []store.GachaPricePhaseEntry
|
||||
for i, cost := range stepCosts {
|
||||
step := int32(i + 1)
|
||||
priceType := model.PriceTypePaidGem
|
||||
if cost == 0 {
|
||||
priceType = model.PriceTypeGem
|
||||
}
|
||||
|
||||
fixedRarityMin := int32(0)
|
||||
fixedCount := int32(0)
|
||||
if step == int32(len(stepCosts)) {
|
||||
fixedRarityMin = model.RaritySSRare
|
||||
fixedCount = 1
|
||||
}
|
||||
|
||||
phases = append(phases, store.GachaPricePhaseEntry{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + step,
|
||||
PriceType: priceType,
|
||||
Price: cost,
|
||||
RegularPrice: model.PremiumMultiPullPrice,
|
||||
DrawCount: model.PremiumMultiPullCount,
|
||||
FixedRarityMin: fixedRarityMin,
|
||||
FixedCount: fixedCount,
|
||||
LimitExecCount: 1,
|
||||
StepNumber: step,
|
||||
})
|
||||
}
|
||||
return phases
|
||||
}
|
||||
|
||||
func buildChapterPricePhases(gachaId int32) []store.GachaPricePhaseEntry {
|
||||
return []store.GachaPricePhaseEntry{
|
||||
{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + 1,
|
||||
PriceType: model.PriceTypeConsumableItem,
|
||||
PriceId: model.ConsumableIdChapterTicket,
|
||||
Price: 1,
|
||||
RegularPrice: 1,
|
||||
DrawCount: 1,
|
||||
},
|
||||
{
|
||||
PhaseId: gachaId*model.PhaseIdMultiplier + 2,
|
||||
PriceType: model.PriceTypeConsumableItem,
|
||||
PriceId: model.ConsumableIdChapterTicket,
|
||||
Price: 10,
|
||||
RegularPrice: 10,
|
||||
DrawCount: model.PremiumMultiPullCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type GachaPoolItem struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
RarityType model.RarityType
|
||||
CharacterId int32
|
||||
}
|
||||
|
||||
type FeaturedSet struct {
|
||||
Costumes []GachaPoolItem
|
||||
Weapons []GachaPoolItem
|
||||
}
|
||||
|
||||
type BannerPool struct {
|
||||
CostumesByRarity map[int32][]GachaPoolItem
|
||||
WeaponsByRarity map[int32][]GachaPoolItem
|
||||
Featured []GachaPoolItem
|
||||
}
|
||||
|
||||
type ShopFeaturedEntry struct {
|
||||
CostumeId int32
|
||||
WeaponId int32
|
||||
}
|
||||
|
||||
type GachaCatalog struct {
|
||||
CostumesByRarity map[int32][]GachaPoolItem
|
||||
WeaponsByRarity map[int32][]GachaPoolItem
|
||||
Materials []GachaPoolItem
|
||||
CostumeById map[int32]GachaPoolItem
|
||||
WeaponById map[int32]GachaPoolItem
|
||||
CostumeWeaponMap map[int32]int32 // costumeId -> paired weaponId
|
||||
FeaturedByGacha map[int32]FeaturedSet
|
||||
BannerPools map[int32]*BannerPool
|
||||
ShopFeaturedByMedal map[int32][]ShopFeaturedEntry // consumableId -> paired entries
|
||||
}
|
||||
|
||||
type costumePoolRow struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
SkillfulWeaponType int32 `json:"SkillfulWeaponType"`
|
||||
RarityType int32 `json:"RarityType"`
|
||||
}
|
||||
|
||||
type weaponPoolRow struct {
|
||||
WeaponId int32 `json:"WeaponId"`
|
||||
WeaponType int32 `json:"WeaponType"`
|
||||
RarityType int32 `json:"RarityType"`
|
||||
IsRestrictDiscard bool `json:"IsRestrictDiscard"`
|
||||
}
|
||||
|
||||
type catalogCostumeRow struct {
|
||||
CostumeId int32 `json:"CostumeId"`
|
||||
CatalogTermId int32 `json:"CatalogTermId"`
|
||||
}
|
||||
|
||||
type catalogWeaponRow struct {
|
||||
WeaponId int32 `json:"WeaponId"`
|
||||
CatalogTermId int32 `json:"CatalogTermId"`
|
||||
}
|
||||
|
||||
type materialPoolRow struct {
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
MaterialType int32 `json:"MaterialType"`
|
||||
RarityType int32 `json:"RarityType"`
|
||||
}
|
||||
|
||||
func LoadGachaPool() (*GachaCatalog, error) {
|
||||
costumes, err := utils.ReadJSON[costumePoolRow]("EntityMCostumeTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume table: %w", err)
|
||||
}
|
||||
weapons, err := utils.ReadJSON[weaponPoolRow]("EntityMWeaponTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon table: %w", err)
|
||||
}
|
||||
catalogCostumes, err := utils.ReadJSON[catalogCostumeRow]("EntityMCatalogCostumeTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load catalog costume table: %w", err)
|
||||
}
|
||||
catalogWeapons, err := utils.ReadJSON[catalogWeaponRow]("EntityMCatalogWeaponTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load catalog weapon table: %w", err)
|
||||
}
|
||||
materials, err := utils.ReadJSON[materialPoolRow]("EntityMMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load material table: %w", err)
|
||||
}
|
||||
evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon evolution group table: %w", err)
|
||||
}
|
||||
evolvedWeapons := buildEvolvedWeaponSet(evoGroupRows)
|
||||
|
||||
catalogCostumeSet := make(map[int32]bool, len(catalogCostumes))
|
||||
costumeTermId := make(map[int32]int32, len(catalogCostumes))
|
||||
for _, c := range catalogCostumes {
|
||||
catalogCostumeSet[c.CostumeId] = true
|
||||
costumeTermId[c.CostumeId] = c.CatalogTermId
|
||||
}
|
||||
catalogWeaponSet := make(map[int32]bool, len(catalogWeapons))
|
||||
for _, w := range catalogWeapons {
|
||||
catalogWeaponSet[w.WeaponId] = true
|
||||
}
|
||||
|
||||
costumeWeaponType := make(map[int32]int32, len(costumes))
|
||||
for _, c := range costumes {
|
||||
costumeWeaponType[c.CostumeId] = c.SkillfulWeaponType
|
||||
}
|
||||
|
||||
weaponTypeById := make(map[int32]int32, len(weapons))
|
||||
weaponRarityById := make(map[int32]int32, len(weapons))
|
||||
restrictedWeapons := make(map[int32]bool)
|
||||
for _, w := range weapons {
|
||||
weaponTypeById[w.WeaponId] = w.WeaponType
|
||||
weaponRarityById[w.WeaponId] = w.RarityType
|
||||
if w.IsRestrictDiscard {
|
||||
restrictedWeapons[w.WeaponId] = true
|
||||
}
|
||||
}
|
||||
|
||||
pool := &GachaCatalog{
|
||||
CostumesByRarity: make(map[int32][]GachaPoolItem),
|
||||
WeaponsByRarity: make(map[int32][]GachaPoolItem),
|
||||
CostumeById: make(map[int32]GachaPoolItem),
|
||||
WeaponById: make(map[int32]GachaPoolItem),
|
||||
CostumeWeaponMap: make(map[int32]int32),
|
||||
FeaturedByGacha: make(map[int32]FeaturedSet),
|
||||
}
|
||||
|
||||
for _, c := range costumes {
|
||||
if !catalogCostumeSet[c.CostumeId] {
|
||||
continue
|
||||
}
|
||||
if c.RarityType < model.RaritySRare {
|
||||
continue
|
||||
}
|
||||
item := GachaPoolItem{
|
||||
PossessionType: int32(model.PossessionTypeCostume),
|
||||
PossessionId: c.CostumeId,
|
||||
RarityType: c.RarityType,
|
||||
CharacterId: c.CharacterId,
|
||||
}
|
||||
pool.CostumesByRarity[c.RarityType] = append(pool.CostumesByRarity[c.RarityType], item)
|
||||
pool.CostumeById[c.CostumeId] = item
|
||||
}
|
||||
|
||||
restrictedCount := 0
|
||||
for _, w := range weapons {
|
||||
if !catalogWeaponSet[w.WeaponId] {
|
||||
continue
|
||||
}
|
||||
if evolvedWeapons[w.WeaponId] {
|
||||
continue
|
||||
}
|
||||
item := GachaPoolItem{
|
||||
PossessionType: int32(model.PossessionTypeWeapon),
|
||||
PossessionId: w.WeaponId,
|
||||
RarityType: w.RarityType,
|
||||
}
|
||||
pool.WeaponById[w.WeaponId] = item
|
||||
if w.IsRestrictDiscard {
|
||||
restrictedCount++
|
||||
continue
|
||||
}
|
||||
pool.WeaponsByRarity[w.RarityType] = append(pool.WeaponsByRarity[w.RarityType], item)
|
||||
}
|
||||
|
||||
log.Printf("[GachaPool] excluded %d evolved weapons, %d restricted weapons from pool", len(evolvedWeapons), restrictedCount)
|
||||
|
||||
type weaponKey struct {
|
||||
TermId int32
|
||||
WeaponType int32
|
||||
Rarity int32
|
||||
}
|
||||
weaponsByKey := make(map[weaponKey][]int32)
|
||||
for _, cw := range catalogWeapons {
|
||||
if evolvedWeapons[cw.WeaponId] || restrictedWeapons[cw.WeaponId] {
|
||||
continue
|
||||
}
|
||||
wt := weaponTypeById[cw.WeaponId]
|
||||
r := weaponRarityById[cw.WeaponId]
|
||||
if wt == 0 || r < model.RaritySRare {
|
||||
continue
|
||||
}
|
||||
k := weaponKey{TermId: cw.CatalogTermId, WeaponType: wt, Rarity: r}
|
||||
weaponsByKey[k] = append(weaponsByKey[k], cw.WeaponId)
|
||||
}
|
||||
for k, ids := range weaponsByKey {
|
||||
slices.Sort(ids)
|
||||
weaponsByKey[k] = ids
|
||||
}
|
||||
|
||||
exact, pattern, bestGuess := 0, 0, 0
|
||||
for costumeId, item := range pool.CostumeById {
|
||||
tid := costumeTermId[costumeId]
|
||||
wt := costumeWeaponType[costumeId]
|
||||
k := weaponKey{TermId: tid, WeaponType: wt, Rarity: item.RarityType}
|
||||
candidates := weaponsByKey[k]
|
||||
if len(candidates) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
pool.CostumeWeaponMap[costumeId] = candidates[0]
|
||||
exact++
|
||||
continue
|
||||
}
|
||||
idPattern := costumeId*10 + 1
|
||||
found := false
|
||||
for _, wid := range candidates {
|
||||
if wid == idPattern {
|
||||
pool.CostumeWeaponMap[costumeId] = wid
|
||||
pattern++
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
pool.CostumeWeaponMap[costumeId] = candidates[0]
|
||||
bestGuess++
|
||||
}
|
||||
}
|
||||
log.Printf("[GachaPool] costume-weapon pairing: %d exact, %d id-pattern, %d best-guess, %d total",
|
||||
exact, pattern, bestGuess, len(pool.CostumeWeaponMap))
|
||||
|
||||
for _, m := range materials {
|
||||
pool.Materials = append(pool.Materials, GachaPoolItem{
|
||||
PossessionType: int32(model.PossessionTypeMaterial),
|
||||
PossessionId: m.MaterialId,
|
||||
RarityType: m.RarityType,
|
||||
})
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func (pool *GachaCatalog) BuildShopFeatured(shop *ShopCatalog) {
|
||||
pool.ShopFeaturedByMedal = make(map[int32][]ShopFeaturedEntry)
|
||||
shopPairs := 0
|
||||
for _, cells := range shop.ExchangeShopCells {
|
||||
consumableId := shop.Items[cells[0].ShopItemId].PriceId
|
||||
|
||||
var entries []ShopFeaturedEntry
|
||||
for _, cell := range cells {
|
||||
contents := shop.Contents[cell.ShopItemId]
|
||||
var costumeId, weaponId int32
|
||||
for _, c := range contents {
|
||||
switch c.PossessionType {
|
||||
case int32(model.PossessionTypeCostume):
|
||||
costumeId = c.PossessionId
|
||||
case int32(model.PossessionTypeWeapon):
|
||||
weaponId = c.PossessionId
|
||||
}
|
||||
}
|
||||
if costumeId == 0 && weaponId == 0 {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, ShopFeaturedEntry{CostumeId: costumeId, WeaponId: weaponId})
|
||||
if costumeId != 0 && weaponId != 0 {
|
||||
pool.CostumeWeaponMap[costumeId] = weaponId
|
||||
shopPairs++
|
||||
}
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
pool.ShopFeaturedByMedal[consumableId] = entries
|
||||
}
|
||||
}
|
||||
log.Printf("[GachaPool] shop featured: %d consumables, %d costume-weapon pairs overridden", len(pool.ShopFeaturedByMedal), shopPairs)
|
||||
}
|
||||
|
||||
func (pool *GachaCatalog) PruneUnpairedCostumes() {
|
||||
pruned := 0
|
||||
for costumeId := range pool.CostumeById {
|
||||
if _, ok := pool.CostumeWeaponMap[costumeId]; !ok {
|
||||
delete(pool.CostumeById, costumeId)
|
||||
pruned++
|
||||
}
|
||||
}
|
||||
for rarity, items := range pool.CostumesByRarity {
|
||||
filtered := items[:0]
|
||||
for _, item := range items {
|
||||
if _, ok := pool.CostumeWeaponMap[item.PossessionId]; ok {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
pool.CostumesByRarity[rarity] = filtered
|
||||
}
|
||||
log.Printf("[GachaPool] pruned %d unpaired costumes", pruned)
|
||||
}
|
||||
|
||||
func (pool *GachaCatalog) BuildFeaturedMapping(entries []store.GachaCatalogEntry) {
|
||||
matched := 0
|
||||
for _, entry := range entries {
|
||||
if entry.MedalConsumableItemId == 0 {
|
||||
continue
|
||||
}
|
||||
shopEntries, ok := pool.ShopFeaturedByMedal[entry.MedalConsumableItemId]
|
||||
if !ok || len(shopEntries) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
seenCostume := make(map[int32]bool)
|
||||
linkedWeapons := make(map[int32]bool)
|
||||
var costumes []GachaPoolItem
|
||||
for _, se := range shopEntries {
|
||||
if se.CostumeId != 0 && !seenCostume[se.CostumeId] {
|
||||
costumes = append(costumes, pool.CostumeById[se.CostumeId])
|
||||
seenCostume[se.CostumeId] = true
|
||||
linkedWeapons[se.WeaponId] = true
|
||||
}
|
||||
}
|
||||
|
||||
seenWeapon := make(map[int32]bool)
|
||||
var weapons []GachaPoolItem
|
||||
for _, se := range shopEntries {
|
||||
if se.WeaponId != 0 && !linkedWeapons[se.WeaponId] && !seenWeapon[se.WeaponId] {
|
||||
if item, ok := pool.WeaponById[se.WeaponId]; ok {
|
||||
weapons = append(weapons, item)
|
||||
seenWeapon[se.WeaponId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pool.FeaturedByGacha[entry.GachaId] = FeaturedSet{Costumes: costumes, Weapons: weapons}
|
||||
matched++
|
||||
}
|
||||
log.Printf("[GachaPool] featured mapping: %d/%d banners matched via shop", matched, len(entries))
|
||||
}
|
||||
|
||||
func (pool *GachaCatalog) BuildBannerPools(entries []store.GachaCatalogEntry) {
|
||||
allFeaturedCostumes := make(map[int32]bool)
|
||||
allFeaturedWeapons := make(map[int32]bool)
|
||||
for _, fs := range pool.FeaturedByGacha {
|
||||
for _, c := range fs.Costumes {
|
||||
allFeaturedCostumes[c.PossessionId] = true
|
||||
allFeaturedWeapons[pool.CostumeWeaponMap[c.PossessionId]] = true
|
||||
}
|
||||
for _, w := range fs.Weapons {
|
||||
allFeaturedWeapons[w.PossessionId] = true
|
||||
}
|
||||
}
|
||||
|
||||
commonCostumes := make(map[int32][]GachaPoolItem)
|
||||
for rarity, items := range pool.CostumesByRarity {
|
||||
for _, item := range items {
|
||||
if !allFeaturedCostumes[item.PossessionId] {
|
||||
commonCostumes[rarity] = append(commonCostumes[rarity], item)
|
||||
}
|
||||
}
|
||||
}
|
||||
commonWeapons := make(map[int32][]GachaPoolItem)
|
||||
for rarity, items := range pool.WeaponsByRarity {
|
||||
for _, item := range items {
|
||||
if !allFeaturedWeapons[item.PossessionId] {
|
||||
commonWeapons[rarity] = append(commonWeapons[rarity], item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commonPool := &BannerPool{
|
||||
CostumesByRarity: commonCostumes,
|
||||
WeaponsByRarity: commonWeapons,
|
||||
}
|
||||
|
||||
pool.BannerPools = make(map[int32]*BannerPool)
|
||||
for _, entry := range entries {
|
||||
fs, hasFeatured := pool.FeaturedByGacha[entry.GachaId]
|
||||
if !hasFeatured {
|
||||
pool.BannerPools[entry.GachaId] = commonPool
|
||||
continue
|
||||
}
|
||||
|
||||
var allFeatured []GachaPoolItem
|
||||
bannerCostumes := make(map[int32][]GachaPoolItem)
|
||||
for rarity, items := range commonCostumes {
|
||||
bannerCostumes[rarity] = append(bannerCostumes[rarity], items...)
|
||||
}
|
||||
bannerWeapons := make(map[int32][]GachaPoolItem)
|
||||
for rarity, items := range commonWeapons {
|
||||
bannerWeapons[rarity] = append(bannerWeapons[rarity], items...)
|
||||
}
|
||||
for _, c := range fs.Costumes {
|
||||
bannerCostumes[c.RarityType] = append(bannerCostumes[c.RarityType], c)
|
||||
allFeatured = append(allFeatured, c)
|
||||
wid := pool.CostumeWeaponMap[c.PossessionId]
|
||||
w := pool.WeaponById[wid]
|
||||
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
||||
allFeatured = append(allFeatured, w)
|
||||
}
|
||||
for _, w := range fs.Weapons {
|
||||
bannerWeapons[w.RarityType] = append(bannerWeapons[w.RarityType], w)
|
||||
allFeatured = append(allFeatured, w)
|
||||
}
|
||||
|
||||
pool.BannerPools[entry.GachaId] = &BannerPool{
|
||||
CostumesByRarity: bannerCostumes,
|
||||
WeaponsByRarity: bannerWeapons,
|
||||
Featured: allFeatured,
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[GachaPool] banner pools: %d banners, %d featured costumes stripped, %d featured weapons stripped",
|
||||
len(pool.BannerPools), len(allFeaturedCostumes), len(allFeaturedWeapons))
|
||||
}
|
||||
|
||||
func buildEvolvedWeaponSet(rows []WeaponEvolutionGroupRow) map[int32]bool {
|
||||
grouped := make(map[int32][]WeaponEvolutionGroupRow)
|
||||
for _, r := range rows {
|
||||
grouped[r.WeaponEvolutionGroupId] = append(grouped[r.WeaponEvolutionGroupId], r)
|
||||
}
|
||||
evolved := make(map[int32]bool)
|
||||
for _, chain := range grouped {
|
||||
sort.Slice(chain, func(i, j int) bool {
|
||||
return chain[i].EvolutionOrder < chain[j].EvolutionOrder
|
||||
})
|
||||
for i := 1; i < len(chain); i++ {
|
||||
evolved[chain[i].WeaponId] = true
|
||||
}
|
||||
}
|
||||
return evolved
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type gimmickScheduleRow struct {
|
||||
GimmickSequenceScheduleId int32 `json:"GimmickSequenceScheduleId"`
|
||||
StartDatetime int64 `json:"StartDatetime"`
|
||||
EndDatetime int64 `json:"EndDatetime"`
|
||||
FirstGimmickSequenceId int32 `json:"FirstGimmickSequenceId"`
|
||||
ReleaseEvaluateConditionId int32 `json:"ReleaseEvaluateConditionId"`
|
||||
}
|
||||
|
||||
type gimmickScheduleEntry struct {
|
||||
ScheduleId int32
|
||||
StartDatetime int64
|
||||
EndDatetime int64
|
||||
FirstSequenceId int32
|
||||
RequiredQuestId int32 // 0 = always active
|
||||
}
|
||||
|
||||
type GimmickCatalog struct {
|
||||
schedules []gimmickScheduleEntry
|
||||
}
|
||||
|
||||
func LoadGimmickCatalog(resolver *ConditionResolver) (*GimmickCatalog, error) {
|
||||
rows, err := utils.ReadJSON[gimmickScheduleRow]("EntityMGimmickSequenceScheduleTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load gimmick sequence schedule table: %w", err)
|
||||
}
|
||||
|
||||
entries := make([]gimmickScheduleEntry, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
entry := gimmickScheduleEntry{
|
||||
ScheduleId: r.GimmickSequenceScheduleId,
|
||||
StartDatetime: r.StartDatetime,
|
||||
EndDatetime: r.EndDatetime,
|
||||
FirstSequenceId: r.FirstGimmickSequenceId,
|
||||
}
|
||||
if r.ReleaseEvaluateConditionId != 0 {
|
||||
if qid, ok := resolver.RequiredQuestId(r.ReleaseEvaluateConditionId); ok {
|
||||
entry.RequiredQuestId = qid
|
||||
}
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
log.Printf("gimmick catalog loaded: %d schedules", len(entries))
|
||||
return &GimmickCatalog{schedules: entries}, nil
|
||||
}
|
||||
|
||||
func (c *GimmickCatalog) ActiveScheduleKeys(user store.UserState, nowMillis int64) []store.GimmickSequenceKey {
|
||||
var keys []store.GimmickSequenceKey
|
||||
for _, s := range c.schedules {
|
||||
if nowMillis < s.StartDatetime || nowMillis > s.EndDatetime {
|
||||
continue
|
||||
}
|
||||
if s.RequiredQuestId != 0 {
|
||||
q, ok := user.Quests[s.RequiredQuestId]
|
||||
if !ok || q.QuestStateType != model.UserQuestStateTypeCleared {
|
||||
continue
|
||||
}
|
||||
}
|
||||
keys = append(keys, store.GimmickSequenceKey{
|
||||
GimmickSequenceScheduleId: s.ScheduleId,
|
||||
GimmickSequenceId: s.FirstSequenceId,
|
||||
})
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type loginBonusStamp struct {
|
||||
LoginBonusId int32 `json:"LoginBonusId"`
|
||||
LowerPageNumber int32 `json:"LowerPageNumber"`
|
||||
StampNumber int32 `json:"StampNumber"`
|
||||
RewardPossessionType int32 `json:"RewardPossessionType"`
|
||||
RewardPossessionId int32 `json:"RewardPossessionId"`
|
||||
RewardCount int32 `json:"RewardCount"`
|
||||
}
|
||||
|
||||
type loginBonusStampKey struct {
|
||||
LoginBonusId int32
|
||||
LowerPageNumber int32
|
||||
StampNumber int32
|
||||
}
|
||||
|
||||
type LoginBonusReward struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
type LoginBonusCatalog struct {
|
||||
stamps map[loginBonusStampKey]LoginBonusReward
|
||||
}
|
||||
|
||||
func (c *LoginBonusCatalog) LookupStampReward(loginBonusId, pageNumber, stampNumber int32) (LoginBonusReward, bool) {
|
||||
entry, ok := c.stamps[loginBonusStampKey{loginBonusId, pageNumber, stampNumber}]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func LoadLoginBonusCatalog() *LoginBonusCatalog {
|
||||
stamps, err := utils.ReadJSON[loginBonusStamp]("EntityMLoginBonusStampTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load login bonus stamp table: %v", err)
|
||||
}
|
||||
|
||||
cat := &LoginBonusCatalog{
|
||||
stamps: make(map[loginBonusStampKey]LoginBonusReward, len(stamps)),
|
||||
}
|
||||
for _, s := range stamps {
|
||||
cat.stamps[loginBonusStampKey{s.LoginBonusId, s.LowerPageNumber, s.StampNumber}] = LoginBonusReward{
|
||||
PossessionType: s.RewardPossessionType,
|
||||
PossessionId: s.RewardPossessionId,
|
||||
Count: s.RewardCount,
|
||||
}
|
||||
}
|
||||
return cat
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type MaterialRow struct {
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
MaterialType model.MaterialType `json:"MaterialType"`
|
||||
WeaponType int32 `json:"WeaponType"`
|
||||
EffectValue int32 `json:"EffectValue"`
|
||||
SellPrice int32 `json:"SellPrice"`
|
||||
}
|
||||
|
||||
type numericalParameterMapRow struct {
|
||||
NumericalParameterMapId int32 `json:"NumericalParameterMapId"`
|
||||
ParameterKey int32 `json:"ParameterKey"`
|
||||
ParameterValue int32 `json:"ParameterValue"`
|
||||
}
|
||||
|
||||
func LoadParameterMap() ([]numericalParameterMapRow, error) {
|
||||
rows, err := utils.ReadJSON[numericalParameterMapRow]("EntityMNumericalParameterMapTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load numerical parameter map table: %w", err)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func BuildExpThresholds(paramMapRows []numericalParameterMapRow, mapId int32) []int32 {
|
||||
maxKey := int32(0)
|
||||
for _, r := range paramMapRows {
|
||||
if r.NumericalParameterMapId == mapId && r.ParameterKey > maxKey {
|
||||
maxKey = r.ParameterKey
|
||||
}
|
||||
}
|
||||
thresholds := make([]int32, maxKey+1)
|
||||
for _, r := range paramMapRows {
|
||||
if r.NumericalParameterMapId == mapId {
|
||||
thresholds[r.ParameterKey] = r.ParameterValue
|
||||
}
|
||||
}
|
||||
return thresholds
|
||||
}
|
||||
|
||||
type MaterialCatalog struct {
|
||||
All map[int32]MaterialRow
|
||||
ByType map[model.MaterialType]map[int32]MaterialRow
|
||||
}
|
||||
|
||||
func LoadMaterialCatalog() (*MaterialCatalog, error) {
|
||||
rows, err := utils.ReadJSON[MaterialRow]("EntityMMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load material table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &MaterialCatalog{
|
||||
All: make(map[int32]MaterialRow, len(rows)),
|
||||
ByType: make(map[model.MaterialType]map[int32]MaterialRow),
|
||||
}
|
||||
for _, row := range rows {
|
||||
catalog.All[row.MaterialId] = row
|
||||
if catalog.ByType[row.MaterialType] == nil {
|
||||
catalog.ByType[row.MaterialType] = make(map[int32]MaterialRow)
|
||||
}
|
||||
catalog.ByType[row.MaterialType][row.MaterialId] = row
|
||||
}
|
||||
return catalog, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type numericalFunctionRow struct {
|
||||
NumericalFunctionId int32 `json:"NumericalFunctionId"`
|
||||
NumericalFunctionType int32 `json:"NumericalFunctionType"`
|
||||
NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"`
|
||||
}
|
||||
|
||||
type numericalFunctionParameterRow struct {
|
||||
NumericalFunctionParameterGroupId int32 `json:"NumericalFunctionParameterGroupId"`
|
||||
ParameterIndex int32 `json:"ParameterIndex"`
|
||||
ParameterValue int32 `json:"ParameterValue"`
|
||||
}
|
||||
|
||||
type NumericalFunc struct {
|
||||
Type model.NumericalFunctionType
|
||||
Params []int32
|
||||
}
|
||||
|
||||
func (f NumericalFunc) Evaluate(value int32) int32 {
|
||||
p := f.Params
|
||||
switch f.Type {
|
||||
case model.NumericalFunctionTypeLinear:
|
||||
return p[1] + p[0]*value
|
||||
case model.NumericalFunctionTypeMonomial:
|
||||
v := value - 1
|
||||
result := v
|
||||
counter := p[1]
|
||||
if counter > 1 {
|
||||
counter--
|
||||
for counter > 0 {
|
||||
counter--
|
||||
result *= v
|
||||
}
|
||||
}
|
||||
return result * p[0]
|
||||
case model.NumericalFunctionTypeLinearPermil:
|
||||
return p[0]*value/1000 + p[1]
|
||||
case model.NumericalFunctionTypePolynomialThird:
|
||||
return p[3] + (p[2]+(p[1]+p[0]*value)*value)*value
|
||||
case model.NumericalFunctionTypePolynomialThirdPermil:
|
||||
return p[0]*value*value*value/1000 +
|
||||
p[1]*value*value/1000 +
|
||||
p[2]*value/1000 +
|
||||
p[3]
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type FunctionResolver struct {
|
||||
functions map[int32]NumericalFunc
|
||||
}
|
||||
|
||||
func LoadFunctionResolver() (*FunctionResolver, error) {
|
||||
funcRows, err := utils.ReadJSON[numericalFunctionRow]("EntityMNumericalFunctionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load numerical function table: %w", err)
|
||||
}
|
||||
|
||||
paramRows, err := utils.ReadJSON[numericalFunctionParameterRow]("EntityMNumericalFunctionParameterGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load numerical function parameter group table: %w", err)
|
||||
}
|
||||
|
||||
paramsByGroup := make(map[int32][]numericalFunctionParameterRow, len(paramRows))
|
||||
for _, r := range paramRows {
|
||||
paramsByGroup[r.NumericalFunctionParameterGroupId] = append(
|
||||
paramsByGroup[r.NumericalFunctionParameterGroupId], r)
|
||||
}
|
||||
for _, group := range paramsByGroup {
|
||||
sort.Slice(group, func(i, j int) bool {
|
||||
return group[i].ParameterIndex < group[j].ParameterIndex
|
||||
})
|
||||
}
|
||||
|
||||
functions := make(map[int32]NumericalFunc, len(funcRows))
|
||||
for _, fr := range funcRows {
|
||||
group := paramsByGroup[fr.NumericalFunctionParameterGroupId]
|
||||
params := make([]int32, len(group))
|
||||
for _, pr := range group {
|
||||
if int(pr.ParameterIndex) < len(params) {
|
||||
params[pr.ParameterIndex] = pr.ParameterValue
|
||||
}
|
||||
}
|
||||
functions[fr.NumericalFunctionId] = NumericalFunc{
|
||||
Type: model.NumericalFunctionType(fr.NumericalFunctionType),
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
return &FunctionResolver{functions: functions}, nil
|
||||
}
|
||||
|
||||
func (r *FunctionResolver) Resolve(functionId int32) (NumericalFunc, bool) {
|
||||
f, ok := r.functions[functionId]
|
||||
return f, ok
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type omikujiEntry struct {
|
||||
OmikujiId int32 `json:"OmikujiId"`
|
||||
OmikujiAssetId int32 `json:"OmikujiAssetId"`
|
||||
}
|
||||
|
||||
type OmikujiCatalog struct {
|
||||
assetIds map[int32]int32
|
||||
}
|
||||
|
||||
func (c *OmikujiCatalog) LookupAssetId(omikujiId int32) int32 {
|
||||
if id, ok := c.assetIds[omikujiId]; ok {
|
||||
return id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func LoadOmikujiCatalog() *OmikujiCatalog {
|
||||
entries, err := utils.ReadJSON[omikujiEntry]("EntityMOmikujiTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load omikuji table: %v", err)
|
||||
}
|
||||
|
||||
cat := &OmikujiCatalog{
|
||||
assetIds: make(map[int32]int32, len(entries)),
|
||||
}
|
||||
for _, e := range entries {
|
||||
cat.assetIds[e.OmikujiId] = e.OmikujiAssetId
|
||||
}
|
||||
return cat
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type PartsRow struct {
|
||||
PartsId int32 `json:"PartsId"`
|
||||
RarityType model.RarityType `json:"RarityType"`
|
||||
PartsGroupId int32 `json:"PartsGroupId"`
|
||||
PartsStatusMainLotteryGroupId int32 `json:"PartsStatusMainLotteryGroupId"`
|
||||
}
|
||||
|
||||
type PartsRarityRow struct {
|
||||
RarityType model.RarityType `json:"RarityType"`
|
||||
PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"`
|
||||
PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"`
|
||||
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
|
||||
}
|
||||
|
||||
type partsLevelUpRateRow struct {
|
||||
PartsLevelUpRateGroupId int32 `json:"PartsLevelUpRateGroupId"`
|
||||
LevelLowerLimit int32 `json:"LevelLowerLimit"`
|
||||
SuccessRatePermil int32 `json:"SuccessRatePermil"`
|
||||
}
|
||||
|
||||
type partsLevelUpPriceRow struct {
|
||||
PartsLevelUpPriceGroupId int32 `json:"PartsLevelUpPriceGroupId"`
|
||||
LevelLowerLimit int32 `json:"LevelLowerLimit"`
|
||||
Gold int32 `json:"Gold"`
|
||||
}
|
||||
|
||||
type PartsCatalog struct {
|
||||
PartsById map[int32]PartsRow
|
||||
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
||||
RarityByRarityType map[model.RarityType]PartsRarityRow
|
||||
RateByGroupAndLevel map[int32]map[int32]int32
|
||||
PriceByGroupAndLevel map[int32]map[int32]int32
|
||||
SellPriceByRarity map[model.RarityType]NumericalFunc
|
||||
}
|
||||
|
||||
func LoadPartsCatalog() (*PartsCatalog, error) {
|
||||
partsRows, err := utils.ReadJSON[PartsRow]("EntityMPartsTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parts table: %w", err)
|
||||
}
|
||||
|
||||
rarityRows, err := utils.ReadJSON[PartsRarityRow]("EntityMPartsRarityTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parts rarity table: %w", err)
|
||||
}
|
||||
|
||||
rateRows, err := utils.ReadJSON[partsLevelUpRateRow]("EntityMPartsLevelUpRateGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parts level up rate table: %w", err)
|
||||
}
|
||||
|
||||
priceRows, err := utils.ReadJSON[partsLevelUpPriceRow]("EntityMPartsLevelUpPriceGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load parts level up price table: %w", err)
|
||||
}
|
||||
|
||||
partsById := make(map[int32]PartsRow, len(partsRows))
|
||||
for _, p := range partsRows {
|
||||
partsById[p.PartsId] = p
|
||||
}
|
||||
|
||||
// Lottery group ID encodes tier (first digit 1-4) and stat category
|
||||
// (second digit 1-6). Formula: mainStatId = (category - 1) * 4 + tier.
|
||||
defaultPartsStatusMainByLotteryGroup := make(map[int32]int32, 24)
|
||||
for tier := int32(1); tier <= 4; tier++ {
|
||||
for cat := int32(1); cat <= 6; cat++ {
|
||||
groupId := tier*10 + cat
|
||||
mainStatId := (cat-1)*4 + tier
|
||||
defaultPartsStatusMainByLotteryGroup[groupId] = mainStatId
|
||||
}
|
||||
}
|
||||
|
||||
funcResolver, err := LoadFunctionResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load function resolver: %w", err)
|
||||
}
|
||||
|
||||
rarityByRarityType := make(map[model.RarityType]PartsRarityRow, len(rarityRows))
|
||||
sellPriceByRarity := make(map[model.RarityType]NumericalFunc, len(rarityRows))
|
||||
for _, r := range rarityRows {
|
||||
rarityByRarityType[r.RarityType] = r
|
||||
if f, ok := funcResolver.Resolve(r.SellPriceNumericalFunctionId); ok {
|
||||
sellPriceByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
|
||||
rateByGroupAndLevel := make(map[int32]map[int32]int32)
|
||||
for _, r := range rateRows {
|
||||
if rateByGroupAndLevel[r.PartsLevelUpRateGroupId] == nil {
|
||||
rateByGroupAndLevel[r.PartsLevelUpRateGroupId] = make(map[int32]int32)
|
||||
}
|
||||
rateByGroupAndLevel[r.PartsLevelUpRateGroupId][r.LevelLowerLimit] = r.SuccessRatePermil
|
||||
}
|
||||
|
||||
priceByGroupAndLevel := make(map[int32]map[int32]int32)
|
||||
for _, p := range priceRows {
|
||||
if priceByGroupAndLevel[p.PartsLevelUpPriceGroupId] == nil {
|
||||
priceByGroupAndLevel[p.PartsLevelUpPriceGroupId] = make(map[int32]int32)
|
||||
}
|
||||
priceByGroupAndLevel[p.PartsLevelUpPriceGroupId][p.LevelLowerLimit] = p.Gold
|
||||
}
|
||||
|
||||
return &PartsCatalog{
|
||||
PartsById: partsById,
|
||||
DefaultPartsStatusMainByLotteryGroup: defaultPartsStatusMainByLotteryGroup,
|
||||
RarityByRarityType: rarityByRarityType,
|
||||
RateByGroupAndLevel: rateByGroupAndLevel,
|
||||
PriceByGroupAndLevel: priceByGroupAndLevel,
|
||||
SellPriceByRarity: sellPriceByRarity,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type QuestSceneRow struct {
|
||||
QuestSceneId int32 `json:"QuestSceneId"`
|
||||
QuestId int32 `json:"QuestId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
QuestSceneType model.QuestSceneType `json:"QuestSceneType"`
|
||||
AssetBackgroundId int32 `json:"AssetBackgroundId"`
|
||||
EventMapNumberUpper int32 `json:"EventMapNumberUpper"`
|
||||
EventMapNumberLower int32 `json:"EventMapNumberLower"`
|
||||
IsMainFlowQuestTarget bool `json:"IsMainFlowQuestTarget"`
|
||||
IsBattleOnlyTarget bool `json:"IsBattleOnlyTarget"`
|
||||
QuestResultType model.QuestResultType `json:"QuestResultType"`
|
||||
IsStorySkipTarget bool `json:"IsStorySkipTarget"`
|
||||
}
|
||||
|
||||
type QuestRow struct {
|
||||
QuestId int32 `json:"QuestId"`
|
||||
NameQuestTextId int32 `json:"NameQuestTextId"`
|
||||
PictureBookNameQuestTextId int32 `json:"PictureBookNameQuestTextId"`
|
||||
QuestReleaseConditionListId int32 `json:"QuestReleaseConditionListId"`
|
||||
StoryQuestTextId int32 `json:"StoryQuestTextId"`
|
||||
QuestDisplayAttributeGroupId int32 `json:"QuestDisplayAttributeGroupId"`
|
||||
RecommendedDeckPower int32 `json:"RecommendedDeckPower"`
|
||||
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
|
||||
QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"`
|
||||
QuestDeckRestrictionGroupId int32 `json:"QuestDeckRestrictionGroupId"`
|
||||
QuestMissionGroupId int32 `json:"QuestMissionGroupId"`
|
||||
Stamina int32 `json:"Stamina"`
|
||||
UserExp int32 `json:"UserExp"`
|
||||
CharacterExp int32 `json:"CharacterExp"`
|
||||
CostumeExp int32 `json:"CostumeExp"`
|
||||
Gold int32 `json:"Gold"`
|
||||
DailyClearableCount int32 `json:"DailyClearableCount"`
|
||||
IsRunInTheBackground bool `json:"IsRunInTheBackground"`
|
||||
IsCountedAsQuest bool `json:"IsCountedAsQuest"`
|
||||
QuestBonusId int32 `json:"QuestBonusId"`
|
||||
IsNotShowAfterClear bool `json:"IsNotShowAfterClear"`
|
||||
IsBigWinTarget bool `json:"IsBigWinTarget"`
|
||||
IsUsableSkipTicket bool `json:"IsUsableSkipTicket"`
|
||||
QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"`
|
||||
InvisibleQuestMissionGroupId int32 `json:"InvisibleQuestMissionGroupId"`
|
||||
FieldEffectGroupId int32 `json:"FieldEffectGroupId"`
|
||||
}
|
||||
|
||||
type QuestMissionRow struct {
|
||||
QuestMissionId int32 `json:"QuestMissionId"`
|
||||
QuestMissionConditionType model.QuestMissionConditionType `json:"QuestMissionConditionType"`
|
||||
QuestMissionRewardId int32 `json:"QuestMissionRewardId"`
|
||||
QuestMissionConditionValueGroupId int32 `json:"QuestMissionConditionValueGroupId"`
|
||||
}
|
||||
|
||||
type QuestMissionGroupRow struct {
|
||||
QuestMissionGroupId int32 `json:"QuestMissionGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
QuestMissionId int32 `json:"QuestMissionId"`
|
||||
}
|
||||
|
||||
type QuestMissionRewardRow struct {
|
||||
QuestMissionRewardId int32 `json:"QuestMissionRewardId"`
|
||||
PossessionType model.PossessionType `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type MainQuestSequenceRow struct {
|
||||
MainQuestSequenceId int32 `json:"MainQuestSequenceId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
QuestId int32 `json:"QuestId"`
|
||||
}
|
||||
|
||||
type MainQuestRouteRow struct {
|
||||
MainQuestRouteId int32 `json:"MainQuestRouteId"`
|
||||
MainQuestSeasonId int32 `json:"MainQuestSeasonId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
CharacterId int32 `json:"CharacterId"`
|
||||
}
|
||||
|
||||
type MainQuestChapterRow struct {
|
||||
MainQuestChapterId int32 `json:"MainQuestChapterId"`
|
||||
MainQuestRouteId int32 `json:"MainQuestRouteId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
MainQuestSequenceGroupId int32 `json:"MainQuestSequenceGroupId"`
|
||||
PortalCageCharacterGroupId int32 `json:"PortalCageCharacterGroupId"`
|
||||
StartDatetime int64 `json:"StartDatetime"`
|
||||
IsInvisibleInLibrary bool `json:"IsInvisibleInLibrary"`
|
||||
JoinLibraryChapterId int32 `json:"JoinLibraryChapterId"`
|
||||
}
|
||||
|
||||
type QuestFirstClearRewardSwitchRow struct {
|
||||
QuestId int32 `json:"QuestId"`
|
||||
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
|
||||
SwitchConditionClearQuestId int32 `json:"SwitchConditionClearQuestId"`
|
||||
}
|
||||
|
||||
type QuestFirstClearRewardGroupRow struct {
|
||||
QuestFirstClearRewardGroupId int32 `json:"QuestFirstClearRewardGroupId"`
|
||||
QuestFirstClearRewardType int32 `json:"QuestFirstClearRewardType"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
PossessionType model.PossessionType `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
IsPickup bool `json:"IsPickup"`
|
||||
}
|
||||
|
||||
type QuestReplayFlowRewardGroupRow struct {
|
||||
QuestReplayFlowRewardGroupId int32 `json:"QuestReplayFlowRewardGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
PossessionType model.PossessionType `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type QuestSceneGrantRow struct {
|
||||
QuestSceneId int32 `json:"QuestSceneId"`
|
||||
PossessionType model.PossessionType `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type QuestPickupRewardGroupRow struct {
|
||||
QuestPickupRewardGroupId int32 `json:"QuestPickupRewardGroupId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
BattleDropRewardId int32 `json:"BattleDropRewardId"`
|
||||
}
|
||||
|
||||
type BattleDropRewardRow struct {
|
||||
BattleDropRewardId int32 `json:"BattleDropRewardId"`
|
||||
PossessionType model.PossessionType `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type QuestSceneBattleRow struct {
|
||||
QuestSceneId int32 `json:"QuestSceneId"`
|
||||
BattleGroupId int32 `json:"BattleGroupId"`
|
||||
}
|
||||
|
||||
type BattleGroupRow struct {
|
||||
BattleGroupId int32 `json:"BattleGroupId"`
|
||||
WaveNumber int32 `json:"WaveNumber"`
|
||||
BattleId int32 `json:"BattleId"`
|
||||
}
|
||||
|
||||
type BattleRow struct {
|
||||
BattleId int32 `json:"BattleId"`
|
||||
BattleNpcId int32 `json:"BattleNpcId"`
|
||||
DeckType model.DeckType `json:"DeckType"`
|
||||
BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"`
|
||||
}
|
||||
|
||||
type BattleNpcDeckRow struct {
|
||||
BattleNpcId int32 `json:"BattleNpcId"`
|
||||
DeckType model.DeckType `json:"DeckType"`
|
||||
BattleNpcDeckNumber int32 `json:"BattleNpcDeckNumber"`
|
||||
BattleNpcDeckCharacterUuid01 string `json:"BattleNpcDeckCharacterUuid01"`
|
||||
BattleNpcDeckCharacterUuid02 string `json:"BattleNpcDeckCharacterUuid02"`
|
||||
BattleNpcDeckCharacterUuid03 string `json:"BattleNpcDeckCharacterUuid03"`
|
||||
}
|
||||
|
||||
type BattleNpcDropCategoryRow struct {
|
||||
BattleNpcId int32 `json:"BattleNpcId"`
|
||||
BattleNpcDeckCharacterUuid string `json:"BattleNpcDeckCharacterUuid"`
|
||||
BattleDropCategoryId int32 `json:"BattleDropCategoryId"`
|
||||
}
|
||||
|
||||
type BattleDropInfo struct {
|
||||
QuestSceneId int32
|
||||
BattleDropCategoryId int32
|
||||
}
|
||||
|
||||
type TutorialUnlockConditionRow struct {
|
||||
TutorialType int32 `json:"TutorialType"`
|
||||
TutorialUnlockConditionType int32 `json:"TutorialUnlockConditionType"`
|
||||
ConditionValue int32 `json:"ConditionValue"`
|
||||
}
|
||||
|
||||
type RentalDeckRow struct {
|
||||
BattleGroupId int32 `json:"BattleGroupId"`
|
||||
}
|
||||
|
||||
type UserLevelRow struct {
|
||||
UserLevel int32 `json:"UserLevel"`
|
||||
MaxStamina int32 `json:"MaxStamina"`
|
||||
}
|
||||
|
||||
type QuestCatalog struct {
|
||||
SceneById map[int32]QuestSceneRow
|
||||
MissionById map[int32]QuestMissionRow
|
||||
QuestById map[int32]QuestRow
|
||||
MissionIdsByQuestId map[int32][]int32
|
||||
RouteIdByQuestId map[int32]int32
|
||||
SceneIdsByQuestId map[int32][]int32
|
||||
OrderedQuestIds []int32
|
||||
FirstClearRewardsByGroupId map[int32][]QuestFirstClearRewardGroupRow
|
||||
FirstClearRewardSwitchesByQuestId map[int32][]QuestFirstClearRewardSwitchRow
|
||||
MissionRewardsByMissionId map[int32][]QuestMissionRewardRow
|
||||
WeaponIdsByReleaseConditionGroupId map[int32][]int32
|
||||
ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow
|
||||
SceneGrantsBySceneId map[int32][]QuestSceneGrantRow
|
||||
BattleDropRewardById map[int32]BattleDropRewardRow
|
||||
PickupRewardIdsByGroupId map[int32][]int32
|
||||
BattleDropsByQuestId map[int32][]BattleDropInfo
|
||||
ReplayFlowRewardsByGroupId map[int32][]QuestReplayFlowRewardGroupRow
|
||||
RentalQuestIds map[int32]bool
|
||||
TutorialUnlockConditions []TutorialUnlockConditionRow
|
||||
ChapterLastSceneByQuestId map[int32]int32
|
||||
SeasonIdByRouteId map[int32]int32
|
||||
|
||||
UserExpThresholds []int32
|
||||
CharacterExpThresholds []int32
|
||||
CostumeExpByRarity map[int32][]int32
|
||||
CostumeMaxLevelByRarity map[int32]NumericalFunc
|
||||
MaxStaminaByLevel map[int32]int32
|
||||
|
||||
CostumeById map[int32]CostumeMasterRow
|
||||
WeaponById map[int32]WeaponMasterRow
|
||||
|
||||
WeaponSkillSlots map[int32][]int32
|
||||
WeaponAbilitySlots map[int32][]int32
|
||||
|
||||
*PartsCatalog
|
||||
}
|
||||
|
||||
func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
||||
scenes, err := utils.ReadJSON[QuestSceneRow]("EntityMQuestSceneTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest scene table: %w", err)
|
||||
}
|
||||
sort.Slice(scenes, func(i, j int) bool {
|
||||
if scenes[i].QuestId != scenes[j].QuestId {
|
||||
return scenes[i].QuestId < scenes[j].QuestId
|
||||
}
|
||||
if scenes[i].SortOrder != scenes[j].SortOrder {
|
||||
return scenes[i].SortOrder < scenes[j].SortOrder
|
||||
}
|
||||
return scenes[i].QuestSceneId < scenes[j].QuestSceneId
|
||||
})
|
||||
|
||||
missions, err := utils.ReadJSON[QuestMissionRow]("EntityMQuestMissionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest mission table: %w", err)
|
||||
}
|
||||
|
||||
quests, err := utils.ReadJSON[QuestRow]("EntityMQuestTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest table: %w", err)
|
||||
}
|
||||
|
||||
missionGroups, err := utils.ReadJSON[QuestMissionGroupRow]("EntityMQuestMissionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest mission group table: %w", err)
|
||||
}
|
||||
sort.Slice(missionGroups, func(i, j int) bool {
|
||||
if missionGroups[i].QuestMissionGroupId != missionGroups[j].QuestMissionGroupId {
|
||||
return missionGroups[i].QuestMissionGroupId < missionGroups[j].QuestMissionGroupId
|
||||
}
|
||||
if missionGroups[i].SortOrder != missionGroups[j].SortOrder {
|
||||
return missionGroups[i].SortOrder < missionGroups[j].SortOrder
|
||||
}
|
||||
return missionGroups[i].QuestMissionId < missionGroups[j].QuestMissionId
|
||||
})
|
||||
|
||||
sequences, err := utils.ReadJSON[MainQuestSequenceRow]("EntityMMainQuestSequenceTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load main quest sequence table: %w", err)
|
||||
}
|
||||
sort.Slice(sequences, func(i, j int) bool {
|
||||
if sequences[i].MainQuestSequenceId != sequences[j].MainQuestSequenceId {
|
||||
return sequences[i].MainQuestSequenceId < sequences[j].MainQuestSequenceId
|
||||
}
|
||||
if sequences[i].SortOrder != sequences[j].SortOrder {
|
||||
return sequences[i].SortOrder < sequences[j].SortOrder
|
||||
}
|
||||
return sequences[i].QuestId < sequences[j].QuestId
|
||||
})
|
||||
|
||||
chapters, err := utils.ReadJSON[MainQuestChapterRow]("EntityMMainQuestChapterTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load main quest chapter table: %w", err)
|
||||
}
|
||||
|
||||
routes, err := utils.ReadJSON[MainQuestRouteRow]("EntityMMainQuestRouteTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load main quest route table: %w", err)
|
||||
}
|
||||
seasonIdByRouteId := make(map[int32]int32, len(routes))
|
||||
for _, r := range routes {
|
||||
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||
}
|
||||
|
||||
firstClearSwitches, err := utils.ReadJSON[QuestFirstClearRewardSwitchRow]("EntityMQuestFirstClearRewardSwitchTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest first clear reward switch table: %w", err)
|
||||
}
|
||||
|
||||
firstClearRewards, err := utils.ReadJSON[QuestFirstClearRewardGroupRow]("EntityMQuestFirstClearRewardGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest first clear reward group table: %w", err)
|
||||
}
|
||||
sort.Slice(firstClearRewards, func(i, j int) bool {
|
||||
if firstClearRewards[i].QuestFirstClearRewardGroupId != firstClearRewards[j].QuestFirstClearRewardGroupId {
|
||||
return firstClearRewards[i].QuestFirstClearRewardGroupId < firstClearRewards[j].QuestFirstClearRewardGroupId
|
||||
}
|
||||
if firstClearRewards[i].SortOrder != firstClearRewards[j].SortOrder {
|
||||
return firstClearRewards[i].SortOrder < firstClearRewards[j].SortOrder
|
||||
}
|
||||
return firstClearRewards[i].QuestFirstClearRewardType < firstClearRewards[j].QuestFirstClearRewardType
|
||||
})
|
||||
|
||||
replayFlowRewards, err := utils.ReadJSON[QuestReplayFlowRewardGroupRow]("EntityMQuestReplayFlowRewardGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest replay flow reward group table: %w", err)
|
||||
}
|
||||
sort.Slice(replayFlowRewards, func(i, j int) bool {
|
||||
if replayFlowRewards[i].QuestReplayFlowRewardGroupId != replayFlowRewards[j].QuestReplayFlowRewardGroupId {
|
||||
return replayFlowRewards[i].QuestReplayFlowRewardGroupId < replayFlowRewards[j].QuestReplayFlowRewardGroupId
|
||||
}
|
||||
return replayFlowRewards[i].SortOrder < replayFlowRewards[j].SortOrder
|
||||
})
|
||||
|
||||
missionRewards, err := utils.ReadJSON[QuestMissionRewardRow]("EntityMQuestMissionRewardTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest mission reward table: %w", err)
|
||||
}
|
||||
|
||||
weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon table: %w", err)
|
||||
}
|
||||
|
||||
weaponSkillGroups, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon skill group table: %w", err)
|
||||
}
|
||||
|
||||
weaponAbilityGroups, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon ability group table: %w", err)
|
||||
}
|
||||
|
||||
releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon story release condition table: %w", err)
|
||||
}
|
||||
|
||||
costumeMasters, err := utils.ReadJSON[CostumeMasterRow]("EntityMCostumeTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume table: %w", err)
|
||||
}
|
||||
|
||||
costumeRarities, err := utils.ReadJSON[costumeRarityRow]("EntityMCostumeRarityTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load costume rarity table: %w", err)
|
||||
}
|
||||
|
||||
sceneGrants, err := utils.ReadJSON[QuestSceneGrantRow]("EntityMUserQuestSceneGrantPossessionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest scene grant table: %w", err)
|
||||
}
|
||||
|
||||
battleDropRewards, err := utils.ReadJSON[BattleDropRewardRow]("EntityMBattleDropRewardTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle drop reward table: %w", err)
|
||||
}
|
||||
|
||||
pickupRewardGroups, err := utils.ReadJSON[QuestPickupRewardGroupRow]("EntityMQuestPickupRewardGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest pickup reward group table: %w", err)
|
||||
}
|
||||
sort.Slice(pickupRewardGroups, func(i, j int) bool {
|
||||
if pickupRewardGroups[i].QuestPickupRewardGroupId != pickupRewardGroups[j].QuestPickupRewardGroupId {
|
||||
return pickupRewardGroups[i].QuestPickupRewardGroupId < pickupRewardGroups[j].QuestPickupRewardGroupId
|
||||
}
|
||||
return pickupRewardGroups[i].SortOrder < pickupRewardGroups[j].SortOrder
|
||||
})
|
||||
|
||||
sceneBattles, err := utils.ReadJSON[QuestSceneBattleRow]("EntityMQuestSceneBattleTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest scene battle table: %w", err)
|
||||
}
|
||||
|
||||
battleGroups, err := utils.ReadJSON[BattleGroupRow]("EntityMBattleGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle group table: %w", err)
|
||||
}
|
||||
|
||||
battles, err := utils.ReadJSON[BattleRow]("EntityMBattleTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle table: %w", err)
|
||||
}
|
||||
|
||||
npcDecks, err := utils.ReadJSON[BattleNpcDeckRow]("EntityMBattleNpcDeckTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle npc deck table: %w", err)
|
||||
}
|
||||
|
||||
npcDropCategories, err := utils.ReadJSON[BattleNpcDropCategoryRow]("EntityMBattleNpcDeckCharacterDropCategoryTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle npc drop category table: %w", err)
|
||||
}
|
||||
|
||||
rentalDecks, err := utils.ReadJSON[RentalDeckRow]("EntityMBattleRentalDeckTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load battle rental deck table: %w", err)
|
||||
}
|
||||
|
||||
tutorialUnlockConds, err := utils.ReadJSON[TutorialUnlockConditionRow]("EntityMTutorialUnlockConditionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
|
||||
}
|
||||
|
||||
paramMapRows, err := LoadParameterMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userLevels, err := utils.ReadJSON[UserLevelRow]("EntityMUserLevelTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load user level table: %w", err)
|
||||
}
|
||||
maxStaminaByLevel := make(map[int32]int32, len(userLevels))
|
||||
for _, ul := range userLevels {
|
||||
maxStaminaByLevel[ul.UserLevel] = ul.MaxStamina
|
||||
}
|
||||
|
||||
funcResolver, err := LoadFunctionResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load function resolver: %w", err)
|
||||
}
|
||||
|
||||
costumeExpByRarity := make(map[int32][]int32, len(costumeRarities))
|
||||
costumeMaxLevelByRarity := make(map[int32]NumericalFunc, len(costumeRarities))
|
||||
for _, r := range costumeRarities {
|
||||
if _, ok := costumeExpByRarity[r.RarityType]; !ok {
|
||||
costumeExpByRarity[r.RarityType] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
|
||||
}
|
||||
if _, ok := costumeMaxLevelByRarity[r.RarityType]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
|
||||
costumeMaxLevelByRarity[r.RarityType] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
costumeById := make(map[int32]CostumeMasterRow, len(costumeMasters))
|
||||
for _, cm := range costumeMasters {
|
||||
costumeById[cm.CostumeId] = cm
|
||||
}
|
||||
|
||||
weaponById := make(map[int32]WeaponMasterRow, len(weapons))
|
||||
for _, w := range weapons {
|
||||
weaponById[w.WeaponId] = w
|
||||
}
|
||||
|
||||
skillSlots := make(map[int32][]int32)
|
||||
for _, row := range weaponSkillGroups {
|
||||
skillSlots[row.WeaponSkillGroupId] = append(skillSlots[row.WeaponSkillGroupId], row.SlotNumber)
|
||||
}
|
||||
abilitySlots := make(map[int32][]int32)
|
||||
for _, row := range weaponAbilityGroups {
|
||||
abilitySlots[row.WeaponAbilityGroupId] = append(abilitySlots[row.WeaponAbilityGroupId], row.SlotNumber)
|
||||
}
|
||||
|
||||
sceneById := make(map[int32]QuestSceneRow, len(scenes))
|
||||
sceneIdsByQuestId := make(map[int32][]int32)
|
||||
for _, scene := range scenes {
|
||||
sceneById[scene.QuestSceneId] = scene
|
||||
sceneIdsByQuestId[scene.QuestId] = append(sceneIdsByQuestId[scene.QuestId], scene.QuestSceneId)
|
||||
}
|
||||
|
||||
missionById := make(map[int32]QuestMissionRow, len(missions))
|
||||
for _, mission := range missions {
|
||||
missionById[mission.QuestMissionId] = mission
|
||||
}
|
||||
|
||||
questById := make(map[int32]QuestRow, len(quests))
|
||||
for _, quest := range quests {
|
||||
questById[quest.QuestId] = quest
|
||||
}
|
||||
|
||||
missionIdsByGroupId := make(map[int32][]int32, len(missionGroups))
|
||||
for _, mg := range missionGroups {
|
||||
missionIdsByGroupId[mg.QuestMissionGroupId] = append(
|
||||
missionIdsByGroupId[mg.QuestMissionGroupId], mg.QuestMissionId)
|
||||
}
|
||||
missionIdsByQuestId := make(map[int32][]int32)
|
||||
for questId, quest := range questById {
|
||||
missionIds := missionIdsByGroupId[quest.QuestMissionGroupId]
|
||||
if len(missionIds) == 0 {
|
||||
continue
|
||||
}
|
||||
missionIdsByQuestId[questId] = append([]int32(nil), missionIds...)
|
||||
}
|
||||
|
||||
chapterBySequenceId := make(map[int32]MainQuestChapterRow, len(chapters))
|
||||
for _, chapter := range chapters {
|
||||
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
||||
}
|
||||
routeIdByQuestId := make(map[int32]int32)
|
||||
for _, sequence := range sequences {
|
||||
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
||||
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
|
||||
}
|
||||
}
|
||||
|
||||
sortedChapters := make([]MainQuestChapterRow, len(chapters))
|
||||
copy(sortedChapters, chapters)
|
||||
sort.Slice(sortedChapters, func(i, j int) bool {
|
||||
return sortedChapters[i].SortOrder < sortedChapters[j].SortOrder
|
||||
})
|
||||
sequencesByGroupId := make(map[int32][]MainQuestSequenceRow)
|
||||
for _, seq := range sequences {
|
||||
sequencesByGroupId[seq.MainQuestSequenceId] = append(sequencesByGroupId[seq.MainQuestSequenceId], seq)
|
||||
}
|
||||
var orderedQuestIds []int32
|
||||
for _, chapter := range sortedChapters {
|
||||
for _, seq := range sequencesByGroupId[chapter.MainQuestSequenceGroupId] {
|
||||
orderedQuestIds = append(orderedQuestIds, seq.QuestId)
|
||||
}
|
||||
}
|
||||
|
||||
chapterLastSceneByQuestId := make(map[int32]int32)
|
||||
for _, chapter := range sortedChapters {
|
||||
seqs := sequencesByGroupId[chapter.MainQuestSequenceGroupId]
|
||||
var chapterLastScene int32
|
||||
for i := len(seqs) - 1; i >= 0; i-- {
|
||||
if sids := sceneIdsByQuestId[seqs[i].QuestId]; len(sids) > 0 {
|
||||
chapterLastScene = sids[len(sids)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if chapterLastScene != 0 {
|
||||
for _, seq := range seqs {
|
||||
chapterLastSceneByQuestId[seq.QuestId] = chapterLastScene
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstClearRewardsByGroupId := make(map[int32][]QuestFirstClearRewardGroupRow, len(firstClearRewards))
|
||||
for _, reward := range firstClearRewards {
|
||||
firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId] = append(
|
||||
firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId], reward)
|
||||
}
|
||||
|
||||
replayFlowRewardsByGroupId := make(map[int32][]QuestReplayFlowRewardGroupRow, len(replayFlowRewards))
|
||||
for _, reward := range replayFlowRewards {
|
||||
replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId] = append(
|
||||
replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId], reward)
|
||||
}
|
||||
|
||||
firstClearRewardSwitchesByQuestId := make(map[int32][]QuestFirstClearRewardSwitchRow, len(firstClearSwitches))
|
||||
for _, switchRow := range firstClearSwitches {
|
||||
firstClearRewardSwitchesByQuestId[switchRow.QuestId] = append(
|
||||
firstClearRewardSwitchesByQuestId[switchRow.QuestId], switchRow)
|
||||
}
|
||||
|
||||
missionRewardsByMissionId := make(map[int32][]QuestMissionRewardRow, len(missionRewards))
|
||||
for _, reward := range missionRewards {
|
||||
missionRewardsByMissionId[reward.QuestMissionRewardId] = append(
|
||||
missionRewardsByMissionId[reward.QuestMissionRewardId], reward)
|
||||
}
|
||||
|
||||
weaponIdsByReleaseConditionGroupId := make(map[int32][]int32)
|
||||
for _, w := range weaponById {
|
||||
if w.WeaponStoryReleaseConditionGroupId != 0 {
|
||||
weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId] = append(
|
||||
weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId], w.WeaponId)
|
||||
}
|
||||
}
|
||||
|
||||
releaseConditionsByGroupId := make(map[int32][]WeaponStoryReleaseConditionRow)
|
||||
for _, c := range releaseConditions {
|
||||
releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append(
|
||||
releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c)
|
||||
}
|
||||
|
||||
sceneGrantsBySceneId := make(map[int32][]QuestSceneGrantRow)
|
||||
for _, sg := range sceneGrants {
|
||||
sceneGrantsBySceneId[sg.QuestSceneId] = append(sceneGrantsBySceneId[sg.QuestSceneId], sg)
|
||||
}
|
||||
|
||||
battleDropRewardById := make(map[int32]BattleDropRewardRow, len(battleDropRewards))
|
||||
for _, bdr := range battleDropRewards {
|
||||
battleDropRewardById[bdr.BattleDropRewardId] = bdr
|
||||
}
|
||||
|
||||
pickupRewardIdsByGroupId := make(map[int32][]int32)
|
||||
for _, pg := range pickupRewardGroups {
|
||||
pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId] = append(
|
||||
pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId], pg.BattleDropRewardId)
|
||||
}
|
||||
|
||||
battleGroupBySceneId := make(map[int32]int32, len(sceneBattles))
|
||||
for _, sb := range sceneBattles {
|
||||
battleGroupBySceneId[sb.QuestSceneId] = sb.BattleGroupId
|
||||
}
|
||||
|
||||
battleIdsByGroupId := make(map[int32][]int32)
|
||||
for _, bg := range battleGroups {
|
||||
battleIdsByGroupId[bg.BattleGroupId] = append(battleIdsByGroupId[bg.BattleGroupId], bg.BattleId)
|
||||
}
|
||||
|
||||
type npcDeckKey struct {
|
||||
BattleNpcId int32
|
||||
DeckType model.DeckType
|
||||
BattleNpcDeckNumber int32
|
||||
}
|
||||
npcDeckByKey := make(map[npcDeckKey]BattleNpcDeckRow, len(npcDecks))
|
||||
for _, d := range npcDecks {
|
||||
npcDeckByKey[npcDeckKey{d.BattleNpcId, d.DeckType, d.BattleNpcDeckNumber}] = d
|
||||
}
|
||||
|
||||
battleByIdMap := make(map[int32]BattleRow, len(battles))
|
||||
for _, b := range battles {
|
||||
battleByIdMap[b.BattleId] = b
|
||||
}
|
||||
|
||||
type dropCatKey struct {
|
||||
BattleNpcId int32
|
||||
Uuid string
|
||||
}
|
||||
dropCategoryByKey := make(map[dropCatKey]int32, len(npcDropCategories))
|
||||
for _, dc := range npcDropCategories {
|
||||
dropCategoryByKey[dropCatKey{dc.BattleNpcId, dc.BattleNpcDeckCharacterUuid}] = dc.BattleDropCategoryId
|
||||
}
|
||||
|
||||
battleDropsByQuestId := make(map[int32][]BattleDropInfo)
|
||||
for questId := range questById {
|
||||
sids := sceneIdsByQuestId[questId]
|
||||
seen := make(map[BattleDropInfo]bool)
|
||||
var drops []BattleDropInfo
|
||||
for _, sceneId := range sids {
|
||||
groupId, ok := battleGroupBySceneId[sceneId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, battleId := range battleIdsByGroupId[groupId] {
|
||||
b, ok := battleByIdMap[battleId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dk := npcDeckKey{b.BattleNpcId, b.DeckType, b.BattleNpcDeckNumber}
|
||||
deck, ok := npcDeckByKey[dk]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, uuid := range []string{deck.BattleNpcDeckCharacterUuid01, deck.BattleNpcDeckCharacterUuid02, deck.BattleNpcDeckCharacterUuid03} {
|
||||
if uuid == "" {
|
||||
continue
|
||||
}
|
||||
catId, ok := dropCategoryByKey[dropCatKey{b.BattleNpcId, uuid}]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
info := BattleDropInfo{QuestSceneId: sceneId, BattleDropCategoryId: catId}
|
||||
if !seen[info] {
|
||||
seen[info] = true
|
||||
drops = append(drops, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(drops) > 0 {
|
||||
battleDropsByQuestId[questId] = drops
|
||||
}
|
||||
}
|
||||
|
||||
rentalBattleGroups := make(map[int32]bool, len(rentalDecks))
|
||||
for _, rd := range rentalDecks {
|
||||
rentalBattleGroups[rd.BattleGroupId] = true
|
||||
}
|
||||
rentalQuestIds := make(map[int32]bool)
|
||||
for questId := range questById {
|
||||
for _, sceneId := range sceneIdsByQuestId[questId] {
|
||||
if groupId, ok := battleGroupBySceneId[sceneId]; ok && rentalBattleGroups[groupId] {
|
||||
rentalQuestIds[questId] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &QuestCatalog{
|
||||
SceneById: sceneById,
|
||||
MissionById: missionById,
|
||||
QuestById: questById,
|
||||
MissionIdsByQuestId: missionIdsByQuestId,
|
||||
RouteIdByQuestId: routeIdByQuestId,
|
||||
SceneIdsByQuestId: sceneIdsByQuestId,
|
||||
OrderedQuestIds: orderedQuestIds,
|
||||
FirstClearRewardsByGroupId: firstClearRewardsByGroupId,
|
||||
FirstClearRewardSwitchesByQuestId: firstClearRewardSwitchesByQuestId,
|
||||
MissionRewardsByMissionId: missionRewardsByMissionId,
|
||||
WeaponIdsByReleaseConditionGroupId: weaponIdsByReleaseConditionGroupId,
|
||||
ReleaseConditionsByGroupId: releaseConditionsByGroupId,
|
||||
SceneGrantsBySceneId: sceneGrantsBySceneId,
|
||||
BattleDropRewardById: battleDropRewardById,
|
||||
PickupRewardIdsByGroupId: pickupRewardIdsByGroupId,
|
||||
BattleDropsByQuestId: battleDropsByQuestId,
|
||||
ReplayFlowRewardsByGroupId: replayFlowRewardsByGroupId,
|
||||
RentalQuestIds: rentalQuestIds,
|
||||
TutorialUnlockConditions: tutorialUnlockConds,
|
||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||
SeasonIdByRouteId: seasonIdByRouteId,
|
||||
|
||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||
CostumeExpByRarity: costumeExpByRarity,
|
||||
CostumeMaxLevelByRarity: costumeMaxLevelByRarity,
|
||||
MaxStaminaByLevel: maxStaminaByLevel,
|
||||
|
||||
CostumeById: costumeById,
|
||||
WeaponById: weaponById,
|
||||
|
||||
WeaponSkillSlots: skillSlots,
|
||||
WeaponAbilitySlots: abilitySlots,
|
||||
|
||||
PartsCatalog: partsCatalog,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type ShopItemRow struct {
|
||||
ShopItemId int32 `json:"ShopItemId"`
|
||||
PriceType int32 `json:"PriceType"`
|
||||
PriceId int32 `json:"PriceId"`
|
||||
Price int32 `json:"Price"`
|
||||
ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"`
|
||||
}
|
||||
|
||||
type ShopContentRow struct {
|
||||
ShopItemId int32 `json:"ShopItemId"`
|
||||
PossessionType int32 `json:"PossessionType"`
|
||||
PossessionId int32 `json:"PossessionId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type ShopContentEffectRow struct {
|
||||
ShopItemId int32 `json:"ShopItemId"`
|
||||
EffectTargetType int32 `json:"EffectTargetType"`
|
||||
EffectValueType int32 `json:"EffectValueType"`
|
||||
EffectValue int32 `json:"EffectValue"`
|
||||
}
|
||||
|
||||
type shopItemLimitedStockRow struct {
|
||||
ShopItemLimitedStockId int32 `json:"ShopItemLimitedStockId"`
|
||||
MaxCount int32 `json:"MaxCount"`
|
||||
}
|
||||
|
||||
type shopRow struct {
|
||||
ShopId int32 `json:"ShopId"`
|
||||
ShopGroupType int32 `json:"ShopGroupType"`
|
||||
ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"`
|
||||
}
|
||||
|
||||
type shopItemCellGroupRow struct {
|
||||
ShopItemCellGroupId int32 `json:"ShopItemCellGroupId"`
|
||||
ShopItemCellId int32 `json:"ShopItemCellId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type shopItemCellRow struct {
|
||||
ShopItemCellId int32 `json:"ShopItemCellId"`
|
||||
ShopItemId int32 `json:"ShopItemId"`
|
||||
}
|
||||
|
||||
type ExchangeShopCell struct {
|
||||
SortOrder int32
|
||||
ShopItemId int32
|
||||
}
|
||||
|
||||
type ShopCatalog struct {
|
||||
Items map[int32]ShopItemRow
|
||||
Contents map[int32][]ShopContentRow
|
||||
Effects map[int32][]ShopContentEffectRow
|
||||
MaxStaminaMillis map[int32]int32 // level -> max stamina in millis
|
||||
LimitedStock map[int32]int32 // stock id -> max count
|
||||
ItemShopPool []int32 // shop item IDs for the replaceable item shop, sorted by cell sort order
|
||||
ExchangeShopCells map[int32][]ExchangeShopCell // shopId -> sorted cells for exchange shops
|
||||
}
|
||||
|
||||
type userLevelEntry struct {
|
||||
UserLevel int32 `json:"UserLevel"`
|
||||
MaxStamina int32 `json:"MaxStamina"`
|
||||
}
|
||||
|
||||
func LoadShopCatalog() (*ShopCatalog, error) {
|
||||
items, err := utils.ReadJSON[ShopItemRow]("EntityMShopItemTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop item table: %w", err)
|
||||
}
|
||||
contents, err := utils.ReadJSON[ShopContentRow]("EntityMShopItemContentPossessionTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop content possession table: %w", err)
|
||||
}
|
||||
effects, err := utils.ReadJSON[ShopContentEffectRow]("EntityMShopItemContentEffectTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop content effect table: %w", err)
|
||||
}
|
||||
userLevels, err := utils.ReadJSON[userLevelEntry]("EntityMUserLevelTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load user level table: %w", err)
|
||||
}
|
||||
stockRows, err := utils.ReadJSON[shopItemLimitedStockRow]("EntityMShopItemLimitedStockTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop item limited stock table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &ShopCatalog{
|
||||
Items: make(map[int32]ShopItemRow, len(items)),
|
||||
Contents: make(map[int32][]ShopContentRow, len(contents)),
|
||||
Effects: make(map[int32][]ShopContentEffectRow, len(effects)),
|
||||
MaxStaminaMillis: make(map[int32]int32, len(userLevels)),
|
||||
LimitedStock: make(map[int32]int32, len(stockRows)),
|
||||
}
|
||||
for _, row := range items {
|
||||
catalog.Items[row.ShopItemId] = row
|
||||
}
|
||||
for _, row := range contents {
|
||||
catalog.Contents[row.ShopItemId] = append(catalog.Contents[row.ShopItemId], row)
|
||||
}
|
||||
for _, row := range effects {
|
||||
catalog.Effects[row.ShopItemId] = append(catalog.Effects[row.ShopItemId], row)
|
||||
}
|
||||
for _, ul := range userLevels {
|
||||
catalog.MaxStaminaMillis[ul.UserLevel] = ul.MaxStamina * 1000
|
||||
}
|
||||
for _, row := range stockRows {
|
||||
catalog.LimitedStock[row.ShopItemLimitedStockId] = row.MaxCount
|
||||
}
|
||||
|
||||
shops, err := utils.ReadJSON[shopRow]("EntityMShopTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop table: %w", err)
|
||||
}
|
||||
cellGroups, err := utils.ReadJSON[shopItemCellGroupRow]("EntityMShopItemCellGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop item cell group table: %w", err)
|
||||
}
|
||||
cells, err := utils.ReadJSON[shopItemCellRow]("EntityMShopItemCellTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load shop item cell table: %w", err)
|
||||
}
|
||||
|
||||
cellIdToItemId := make(map[int32]int32, len(cells))
|
||||
for _, c := range cells {
|
||||
cellIdToItemId[c.ShopItemCellId] = c.ShopItemId
|
||||
}
|
||||
|
||||
cellGroupByCGId := make(map[int32][]shopItemCellGroupRow, len(cellGroups))
|
||||
for _, cg := range cellGroups {
|
||||
cellGroupByCGId[cg.ShopItemCellGroupId] = append(cellGroupByCGId[cg.ShopItemCellGroupId], cg)
|
||||
}
|
||||
|
||||
catalog.ExchangeShopCells = make(map[int32][]ExchangeShopCell)
|
||||
for _, s := range shops {
|
||||
entries := cellGroupByCGId[s.ShopItemCellGroupId]
|
||||
if len(entries) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch s.ShopGroupType {
|
||||
case model.ShopGroupTypeItemShop:
|
||||
var poolCells []ExchangeShopCell
|
||||
for _, cg := range entries {
|
||||
if itemId, ok := cellIdToItemId[cg.ShopItemCellId]; ok {
|
||||
poolCells = append(poolCells, ExchangeShopCell{cg.SortOrder, itemId})
|
||||
}
|
||||
}
|
||||
sort.Slice(poolCells, func(i, j int) bool { return poolCells[i].SortOrder < poolCells[j].SortOrder })
|
||||
catalog.ItemShopPool = make([]int32, len(poolCells))
|
||||
for i, pc := range poolCells {
|
||||
catalog.ItemShopPool[i] = pc.ShopItemId
|
||||
}
|
||||
|
||||
case model.ShopGroupTypeExchangeShop:
|
||||
var sc []ExchangeShopCell
|
||||
for _, cg := range entries {
|
||||
if itemId, ok := cellIdToItemId[cg.ShopItemCellId]; ok {
|
||||
sc = append(sc, ExchangeShopCell{cg.SortOrder, itemId})
|
||||
}
|
||||
}
|
||||
sort.Slice(sc, func(i, j int) bool { return sc[i].SortOrder < sc[j].SortOrder })
|
||||
catalog.ExchangeShopCells[s.ShopId] = sc
|
||||
}
|
||||
}
|
||||
|
||||
return catalog, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type sideStorySceneRow struct {
|
||||
SideStoryQuestId int32 `json:"SideStoryQuestId"`
|
||||
SideStoryQuestSceneId int32 `json:"SideStoryQuestSceneId"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type SideStoryCatalog struct {
|
||||
FirstSceneByQuestId map[int32]int32
|
||||
}
|
||||
|
||||
func LoadSideStoryCatalog() *SideStoryCatalog {
|
||||
scenes, err := utils.ReadJSON[sideStorySceneRow]("EntityMSideStoryQuestSceneTable.json")
|
||||
if err != nil {
|
||||
log.Fatalf("load side story quest scene table: %v", err)
|
||||
}
|
||||
|
||||
firstScene := make(map[int32]int32, len(scenes)/7)
|
||||
for _, s := range scenes {
|
||||
if s.SortOrder == 1 {
|
||||
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("side story catalog loaded: %d quests", len(firstScene))
|
||||
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type WeaponMasterRow struct {
|
||||
WeaponId int32 `json:"WeaponId"`
|
||||
RarityType int32 `json:"RarityType"`
|
||||
WeaponType int32 `json:"WeaponType"`
|
||||
WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"`
|
||||
WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"`
|
||||
WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"`
|
||||
WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"`
|
||||
WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"`
|
||||
}
|
||||
|
||||
type WeaponStoryReleaseConditionRow struct {
|
||||
WeaponStoryReleaseConditionGroupId int32 `json:"WeaponStoryReleaseConditionGroupId"`
|
||||
StoryIndex int32 `json:"StoryIndex"`
|
||||
WeaponStoryReleaseConditionType model.WeaponStoryReleaseConditionType `json:"WeaponStoryReleaseConditionType"`
|
||||
ConditionValue int32 `json:"ConditionValue"`
|
||||
WeaponStoryReleaseConditionOperationGroupId int32 `json:"WeaponStoryReleaseConditionOperationGroupId"`
|
||||
}
|
||||
|
||||
type WeaponSkillGroupRow struct {
|
||||
WeaponSkillGroupId int32 `json:"WeaponSkillGroupId"`
|
||||
SlotNumber int32 `json:"SlotNumber"`
|
||||
SkillId int32 `json:"SkillId"`
|
||||
WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"`
|
||||
}
|
||||
|
||||
type WeaponAbilityGroupRow struct {
|
||||
WeaponAbilityGroupId int32 `json:"WeaponAbilityGroupId"`
|
||||
SlotNumber int32 `json:"SlotNumber"`
|
||||
AbilityId int32 `json:"AbilityId"`
|
||||
WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"`
|
||||
}
|
||||
|
||||
type weaponSpecificEnhanceRow struct {
|
||||
WeaponSpecificEnhanceId int32 `json:"WeaponSpecificEnhanceId"`
|
||||
BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"`
|
||||
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
|
||||
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
|
||||
EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"`
|
||||
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
|
||||
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
|
||||
EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"`
|
||||
LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"`
|
||||
LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"`
|
||||
MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"`
|
||||
SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"`
|
||||
MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"`
|
||||
AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"`
|
||||
}
|
||||
|
||||
type weaponConsumeExchangeRow struct {
|
||||
WeaponId int32 `json:"WeaponId"`
|
||||
ConsumableItemId int32 `json:"ConsumableItemId"`
|
||||
Count int32 `json:"Count"`
|
||||
}
|
||||
|
||||
type WeaponEvolutionGroupRow struct {
|
||||
WeaponEvolutionGroupId int32 `json:"WeaponEvolutionGroupId"`
|
||||
EvolutionOrder int32 `json:"EvolutionOrder"`
|
||||
WeaponId int32 `json:"WeaponId"`
|
||||
}
|
||||
|
||||
type WeaponEvolutionMaterialRow struct {
|
||||
WeaponEvolutionMaterialGroupId int32 `json:"WeaponEvolutionMaterialGroupId"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type WeaponSkillEnhanceMaterialRow struct {
|
||||
WeaponSkillEnhancementMaterialId int32 `json:"WeaponSkillEnhancementMaterialId"`
|
||||
SkillLevel int32 `json:"SkillLevel"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type WeaponAbilityEnhanceMaterialRow struct {
|
||||
WeaponAbilityEnhancementMaterialId int32 `json:"WeaponAbilityEnhancementMaterialId"`
|
||||
AbilityLevel int32 `json:"AbilityLevel"`
|
||||
MaterialId int32 `json:"MaterialId"`
|
||||
Count int32 `json:"Count"`
|
||||
SortOrder int32 `json:"SortOrder"`
|
||||
}
|
||||
|
||||
type weaponRarityEnhanceRow struct {
|
||||
RarityType int32 `json:"RarityType"`
|
||||
BaseEnhancementObtainedExp int32 `json:"BaseEnhancementObtainedExp"`
|
||||
SellPriceNumericalFunctionId int32 `json:"SellPriceNumericalFunctionId"`
|
||||
RequiredExpForLevelUpNumericalParameterMapId int32 `json:"RequiredExpForLevelUpNumericalParameterMapId"`
|
||||
EnhancementCostByWeaponNumericalFunctionId int32 `json:"EnhancementCostByWeaponNumericalFunctionId"`
|
||||
EnhancementCostByMaterialNumericalFunctionId int32 `json:"EnhancementCostByMaterialNumericalFunctionId"`
|
||||
MaxLevelNumericalFunctionId int32 `json:"MaxLevelNumericalFunctionId"`
|
||||
EvolutionCostNumericalFunctionId int32 `json:"EvolutionCostNumericalFunctionId"`
|
||||
LimitBreakCostByWeaponNumericalFunctionId int32 `json:"LimitBreakCostByWeaponNumericalFunctionId"`
|
||||
LimitBreakCostByMaterialNumericalFunctionId int32 `json:"LimitBreakCostByMaterialNumericalFunctionId"`
|
||||
MaxSkillLevelNumericalFunctionId int32 `json:"MaxSkillLevelNumericalFunctionId"`
|
||||
SkillEnhancementCostNumericalFunctionId int32 `json:"SkillEnhancementCostNumericalFunctionId"`
|
||||
MaxAbilityLevelNumericalFunctionId int32 `json:"MaxAbilityLevelNumericalFunctionId"`
|
||||
AbilityEnhancementCostNumericalFunctionId int32 `json:"AbilityEnhancementCostNumericalFunctionId"`
|
||||
}
|
||||
|
||||
type WeaponCatalog struct {
|
||||
Weapons map[int32]WeaponMasterRow
|
||||
Materials map[int32]MaterialRow
|
||||
ExpByEnhanceId map[int32][]int32
|
||||
GoldCostByEnhanceId map[int32]NumericalFunc
|
||||
MaxLevelByEnhanceId map[int32]NumericalFunc
|
||||
SellPriceByEnhanceId map[int32]NumericalFunc
|
||||
MedalsByWeaponId map[int32]map[int32]int32 // WeaponId -> ConsumableItemId -> Count
|
||||
EvolutionNextWeaponId map[int32]int32
|
||||
EvolutionOrder map[int32]int32 // WeaponId -> 0-based position in evolution chain
|
||||
EvolutionMaterials map[int32][]WeaponEvolutionMaterialRow // WeaponEvolutionMaterialGroupId -> materials
|
||||
EvolutionCostByEnhanceId map[int32]NumericalFunc
|
||||
AbilitySlots map[int32][]int32 // WeaponAbilityGroupId -> slot numbers
|
||||
SkillGroupsByGroupId map[int32][]WeaponSkillGroupRow
|
||||
SkillEnhanceMats map[[2]int32][]WeaponSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
|
||||
SkillMaxLevelByEnhanceId map[int32]NumericalFunc
|
||||
SkillCostByEnhanceId map[int32]NumericalFunc
|
||||
AbilityGroupsByGroupId map[int32][]WeaponAbilityGroupRow
|
||||
AbilityEnhanceMats map[[2]int32][]WeaponAbilityEnhanceMaterialRow // key: [enhancementMaterialId, abilityLevel]
|
||||
AbilityMaxLevelByEnhanceId map[int32]NumericalFunc
|
||||
AbilityCostByEnhanceId map[int32]NumericalFunc
|
||||
EnhanceCostByWeaponByEnhanceId map[int32]NumericalFunc
|
||||
LimitBreakCostByWeaponByEnhanceId map[int32]NumericalFunc
|
||||
LimitBreakCostByMaterialByEnhanceId map[int32]NumericalFunc
|
||||
BaseExpByEnhanceId map[int32]int32
|
||||
ReleaseConditionsByGroupId map[int32][]WeaponStoryReleaseConditionRow
|
||||
}
|
||||
|
||||
func LoadWeaponCatalog(matCatalog *MaterialCatalog) (*WeaponCatalog, error) {
|
||||
weapons, err := utils.ReadJSON[WeaponMasterRow]("EntityMWeaponTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon table: %w", err)
|
||||
}
|
||||
|
||||
enhanceRows, err := utils.ReadJSON[weaponSpecificEnhanceRow]("EntityMWeaponSpecificEnhanceTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon specific enhance table: %w", err)
|
||||
}
|
||||
|
||||
rarityEnhanceRows, err := utils.ReadJSON[weaponRarityEnhanceRow]("EntityMWeaponRarityTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon rarity table: %w", err)
|
||||
}
|
||||
|
||||
paramMapRows, err := LoadParameterMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
funcResolver, err := LoadFunctionResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load function resolver: %w", err)
|
||||
}
|
||||
|
||||
exchangeRows, err := utils.ReadJSON[weaponConsumeExchangeRow]("EntityMWeaponConsumeExchangeConsumableItemGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon consume exchange table: %w", err)
|
||||
}
|
||||
|
||||
evoGroupRows, err := utils.ReadJSON[WeaponEvolutionGroupRow]("EntityMWeaponEvolutionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon evolution group table: %w", err)
|
||||
}
|
||||
evoMatRows, err := utils.ReadJSON[WeaponEvolutionMaterialRow]("EntityMWeaponEvolutionMaterialGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon evolution material group table: %w", err)
|
||||
}
|
||||
abilityGroupRows, err := utils.ReadJSON[WeaponAbilityGroupRow]("EntityMWeaponAbilityGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon ability group table: %w", err)
|
||||
}
|
||||
skillGroupRows, err := utils.ReadJSON[WeaponSkillGroupRow]("EntityMWeaponSkillGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon skill group table: %w", err)
|
||||
}
|
||||
skillMatRows, err := utils.ReadJSON[WeaponSkillEnhanceMaterialRow]("EntityMWeaponSkillEnhancementMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon skill enhancement material table: %w", err)
|
||||
}
|
||||
abilityMatRows, err := utils.ReadJSON[WeaponAbilityEnhanceMaterialRow]("EntityMWeaponAbilityEnhancementMaterialTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon ability enhancement material table: %w", err)
|
||||
}
|
||||
releaseConditions, err := utils.ReadJSON[WeaponStoryReleaseConditionRow]("EntityMWeaponStoryReleaseConditionGroupTable.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load weapon story release condition table: %w", err)
|
||||
}
|
||||
|
||||
catalog := &WeaponCatalog{
|
||||
Weapons: make(map[int32]WeaponMasterRow, len(weapons)),
|
||||
Materials: matCatalog.ByType[model.MaterialTypeWeaponEnhancement],
|
||||
ExpByEnhanceId: make(map[int32][]int32, len(enhanceRows)),
|
||||
GoldCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
MaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
SellPriceByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
MedalsByWeaponId: make(map[int32]map[int32]int32),
|
||||
EvolutionNextWeaponId: make(map[int32]int32),
|
||||
EvolutionOrder: make(map[int32]int32),
|
||||
EvolutionMaterials: make(map[int32][]WeaponEvolutionMaterialRow),
|
||||
EvolutionCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
AbilitySlots: make(map[int32][]int32),
|
||||
SkillGroupsByGroupId: make(map[int32][]WeaponSkillGroupRow),
|
||||
SkillEnhanceMats: make(map[[2]int32][]WeaponSkillEnhanceMaterialRow),
|
||||
SkillMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
SkillCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
AbilityGroupsByGroupId: make(map[int32][]WeaponAbilityGroupRow),
|
||||
AbilityEnhanceMats: make(map[[2]int32][]WeaponAbilityEnhanceMaterialRow),
|
||||
AbilityMaxLevelByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
AbilityCostByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
EnhanceCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
LimitBreakCostByWeaponByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
LimitBreakCostByMaterialByEnhanceId: make(map[int32]NumericalFunc, len(enhanceRows)),
|
||||
BaseExpByEnhanceId: make(map[int32]int32, len(enhanceRows)),
|
||||
ReleaseConditionsByGroupId: make(map[int32][]WeaponStoryReleaseConditionRow),
|
||||
}
|
||||
|
||||
for _, w := range weapons {
|
||||
catalog.Weapons[w.WeaponId] = w
|
||||
}
|
||||
|
||||
for _, r := range enhanceRows {
|
||||
if _, ok := catalog.ExpByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
catalog.ExpByEnhanceId[r.WeaponSpecificEnhanceId] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
|
||||
}
|
||||
if _, ok := catalog.GoldCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
|
||||
catalog.GoldCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.MaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
|
||||
catalog.MaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.SellPriceByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.SellPriceNumericalFunctionId); found {
|
||||
catalog.SellPriceByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.EvolutionCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.EvolutionCostNumericalFunctionId); found {
|
||||
catalog.EvolutionCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.SkillMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.MaxSkillLevelNumericalFunctionId); found {
|
||||
catalog.SkillMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.SkillCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.SkillEnhancementCostNumericalFunctionId); found {
|
||||
catalog.SkillCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.AbilityMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.MaxAbilityLevelNumericalFunctionId); found {
|
||||
catalog.AbilityMaxLevelByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.AbilityCostByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.AbilityEnhancementCostNumericalFunctionId); found {
|
||||
catalog.AbilityCostByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.EnhanceCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.EnhancementCostByWeaponNumericalFunctionId); found {
|
||||
catalog.EnhanceCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.LimitBreakCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.LimitBreakCostByWeaponNumericalFunctionId); found {
|
||||
catalog.LimitBreakCostByWeaponByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.LimitBreakCostByMaterialByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
if f, found := funcResolver.Resolve(r.LimitBreakCostByMaterialNumericalFunctionId); found {
|
||||
catalog.LimitBreakCostByMaterialByEnhanceId[r.WeaponSpecificEnhanceId] = f
|
||||
}
|
||||
}
|
||||
if _, ok := catalog.BaseExpByEnhanceId[r.WeaponSpecificEnhanceId]; !ok {
|
||||
catalog.BaseExpByEnhanceId[r.WeaponSpecificEnhanceId] = r.BaseEnhancementObtainedExp
|
||||
}
|
||||
}
|
||||
|
||||
for _, ex := range exchangeRows {
|
||||
if catalog.MedalsByWeaponId[ex.WeaponId] == nil {
|
||||
catalog.MedalsByWeaponId[ex.WeaponId] = make(map[int32]int32)
|
||||
}
|
||||
catalog.MedalsByWeaponId[ex.WeaponId][ex.ConsumableItemId] = ex.Count
|
||||
}
|
||||
|
||||
grouped := make(map[int32][]WeaponEvolutionGroupRow)
|
||||
for _, row := range evoGroupRows {
|
||||
grouped[row.WeaponEvolutionGroupId] = append(grouped[row.WeaponEvolutionGroupId], row)
|
||||
}
|
||||
for _, rows := range grouped {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].EvolutionOrder < rows[j].EvolutionOrder
|
||||
})
|
||||
for i, row := range rows {
|
||||
catalog.EvolutionOrder[row.WeaponId] = int32(i)
|
||||
if i < len(rows)-1 {
|
||||
catalog.EvolutionNextWeaponId[row.WeaponId] = rows[i+1].WeaponId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range evoMatRows {
|
||||
catalog.EvolutionMaterials[row.WeaponEvolutionMaterialGroupId] = append(
|
||||
catalog.EvolutionMaterials[row.WeaponEvolutionMaterialGroupId], row)
|
||||
}
|
||||
|
||||
for _, row := range abilityGroupRows {
|
||||
catalog.AbilitySlots[row.WeaponAbilityGroupId] = append(
|
||||
catalog.AbilitySlots[row.WeaponAbilityGroupId], row.SlotNumber)
|
||||
}
|
||||
|
||||
for _, row := range skillGroupRows {
|
||||
catalog.SkillGroupsByGroupId[row.WeaponSkillGroupId] = append(
|
||||
catalog.SkillGroupsByGroupId[row.WeaponSkillGroupId], row)
|
||||
}
|
||||
|
||||
for _, row := range skillMatRows {
|
||||
key := [2]int32{row.WeaponSkillEnhancementMaterialId, row.SkillLevel}
|
||||
catalog.SkillEnhanceMats[key] = append(catalog.SkillEnhanceMats[key], row)
|
||||
}
|
||||
|
||||
for _, row := range abilityGroupRows {
|
||||
catalog.AbilityGroupsByGroupId[row.WeaponAbilityGroupId] = append(
|
||||
catalog.AbilityGroupsByGroupId[row.WeaponAbilityGroupId], row)
|
||||
}
|
||||
|
||||
for _, row := range abilityMatRows {
|
||||
key := [2]int32{row.WeaponAbilityEnhancementMaterialId, row.AbilityLevel}
|
||||
catalog.AbilityEnhanceMats[key] = append(catalog.AbilityEnhanceMats[key], row)
|
||||
}
|
||||
|
||||
for _, c := range releaseConditions {
|
||||
catalog.ReleaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append(
|
||||
catalog.ReleaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c)
|
||||
}
|
||||
|
||||
// Rarity-based enhancement fallback: for weapons with WeaponSpecificEnhanceId == 0,
|
||||
// use EntityMWeaponRarityTable curves via synthetic enhance IDs (-RarityType).
|
||||
rarityByType := make(map[int32]weaponRarityEnhanceRow, len(rarityEnhanceRows))
|
||||
for _, r := range rarityEnhanceRows {
|
||||
rarityByType[r.RarityType] = r
|
||||
}
|
||||
|
||||
registeredRarity := make(map[int32]bool, len(rarityEnhanceRows))
|
||||
fallbackCount := 0
|
||||
for wid, w := range catalog.Weapons {
|
||||
if w.WeaponSpecificEnhanceId != 0 {
|
||||
continue
|
||||
}
|
||||
syntheticId := -w.RarityType
|
||||
if !registeredRarity[w.RarityType] {
|
||||
r, ok := rarityByType[w.RarityType]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
catalog.ExpByEnhanceId[syntheticId] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId)
|
||||
if f, found := funcResolver.Resolve(r.EnhancementCostByMaterialNumericalFunctionId); found {
|
||||
catalog.GoldCostByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found {
|
||||
catalog.MaxLevelByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.SellPriceNumericalFunctionId); found {
|
||||
catalog.SellPriceByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.EvolutionCostNumericalFunctionId); found {
|
||||
catalog.EvolutionCostByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.MaxSkillLevelNumericalFunctionId); found {
|
||||
catalog.SkillMaxLevelByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.SkillEnhancementCostNumericalFunctionId); found {
|
||||
catalog.SkillCostByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.MaxAbilityLevelNumericalFunctionId); found {
|
||||
catalog.AbilityMaxLevelByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.AbilityEnhancementCostNumericalFunctionId); found {
|
||||
catalog.AbilityCostByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.EnhancementCostByWeaponNumericalFunctionId); found {
|
||||
catalog.EnhanceCostByWeaponByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.LimitBreakCostByWeaponNumericalFunctionId); found {
|
||||
catalog.LimitBreakCostByWeaponByEnhanceId[syntheticId] = f
|
||||
}
|
||||
if f, found := funcResolver.Resolve(r.LimitBreakCostByMaterialNumericalFunctionId); found {
|
||||
catalog.LimitBreakCostByMaterialByEnhanceId[syntheticId] = f
|
||||
}
|
||||
catalog.BaseExpByEnhanceId[syntheticId] = r.BaseEnhancementObtainedExp
|
||||
registeredRarity[w.RarityType] = true
|
||||
}
|
||||
w.WeaponSpecificEnhanceId = syntheticId
|
||||
catalog.Weapons[wid] = w
|
||||
fallbackCount++
|
||||
}
|
||||
log.Printf("[WeaponCatalog] rarity fallback: assigned synthetic enhance IDs to %d weapons", fallbackCount)
|
||||
|
||||
return catalog, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package model
|
||||
|
||||
type CharacterBoardEffectType int32
|
||||
|
||||
const (
|
||||
CharacterBoardEffectTypeUnknown CharacterBoardEffectType = 0
|
||||
CharacterBoardEffectTypeAbility CharacterBoardEffectType = 1
|
||||
CharacterBoardEffectTypeStatusUp CharacterBoardEffectType = 2
|
||||
)
|
||||
|
||||
type CharacterBoardStatusUpType int32
|
||||
|
||||
const (
|
||||
CharacterBoardStatusUpTypeUnknown CharacterBoardStatusUpType = 0
|
||||
CharacterBoardStatusUpTypeAgilityAdd CharacterBoardStatusUpType = 1
|
||||
CharacterBoardStatusUpTypeAgilityMultiply CharacterBoardStatusUpType = 2
|
||||
CharacterBoardStatusUpTypeAttackAdd CharacterBoardStatusUpType = 3
|
||||
CharacterBoardStatusUpTypeAttackMultiply CharacterBoardStatusUpType = 4
|
||||
CharacterBoardStatusUpTypeCritAttackAdd CharacterBoardStatusUpType = 5
|
||||
CharacterBoardStatusUpTypeCritRatioAdd CharacterBoardStatusUpType = 6
|
||||
CharacterBoardStatusUpTypeHpAdd CharacterBoardStatusUpType = 7
|
||||
CharacterBoardStatusUpTypeHpMultiply CharacterBoardStatusUpType = 8
|
||||
CharacterBoardStatusUpTypeVitalityAdd CharacterBoardStatusUpType = 9
|
||||
CharacterBoardStatusUpTypeVitalityMultiply CharacterBoardStatusUpType = 10
|
||||
)
|
||||
|
||||
type StatusCalculationType int32
|
||||
|
||||
const (
|
||||
StatusCalculationTypeUnknown StatusCalculationType = 0
|
||||
StatusCalculationTypeAdd StatusCalculationType = 1
|
||||
StatusCalculationTypeMultiply StatusCalculationType = 2
|
||||
)
|
||||
|
||||
func StatusUpTypeToCalcType(t CharacterBoardStatusUpType) StatusCalculationType {
|
||||
switch t {
|
||||
case CharacterBoardStatusUpTypeAgilityMultiply,
|
||||
CharacterBoardStatusUpTypeAttackMultiply,
|
||||
CharacterBoardStatusUpTypeHpMultiply,
|
||||
CharacterBoardStatusUpTypeVitalityMultiply:
|
||||
return StatusCalculationTypeMultiply
|
||||
default:
|
||||
return StatusCalculationTypeAdd
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
type DeckType int32
|
||||
|
||||
const (
|
||||
DeckTypeUnknown DeckType = 0
|
||||
DeckTypeQuest DeckType = 1
|
||||
DeckTypePvp DeckType = 2
|
||||
DeckTypeMulti DeckType = 3
|
||||
DeckTypeRestrictedQuest DeckType = 4
|
||||
DeckTypeBigHunt DeckType = 5
|
||||
DeckTypeRestrictedLimitContentQuest DeckType = 6
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
const (
|
||||
EffectTargetUnknown int32 = 0
|
||||
EffectTargetStaminaRecovery int32 = 1
|
||||
EffectTargetBattlePointRecovery int32 = 2
|
||||
)
|
||||
|
||||
const (
|
||||
EffectValueUnknown int32 = 0
|
||||
EffectValueFixed int32 = 1
|
||||
EffectValuePermil int32 = 2
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
type EvaluateConditionFunctionType int32
|
||||
|
||||
const (
|
||||
EvaluateConditionFunctionTypeUnknown EvaluateConditionFunctionType = 0
|
||||
EvaluateConditionFunctionTypeRecursion EvaluateConditionFunctionType = 1
|
||||
EvaluateConditionFunctionTypeCageTreasureHunt EvaluateConditionFunctionType = 2
|
||||
EvaluateConditionFunctionTypeCageIntervalDropItem EvaluateConditionFunctionType = 3
|
||||
EvaluateConditionFunctionTypeQuestClear EvaluateConditionFunctionType = 4
|
||||
EvaluateConditionFunctionTypeGimmickBitCount EvaluateConditionFunctionType = 5
|
||||
EvaluateConditionFunctionTypeWeaponAcquisition EvaluateConditionFunctionType = 6
|
||||
EvaluateConditionFunctionTypeTutorial EvaluateConditionFunctionType = 7
|
||||
EvaluateConditionFunctionTypeMissionClear EvaluateConditionFunctionType = 8
|
||||
EvaluateConditionFunctionTypeQuestMissionClear EvaluateConditionFunctionType = 9
|
||||
EvaluateConditionFunctionTypeOtherGimmickBitCount EvaluateConditionFunctionType = 10
|
||||
EvaluateConditionFunctionTypeQuestSceneChoice EvaluateConditionFunctionType = 11
|
||||
EvaluateConditionFunctionTypeQuestNotClear EvaluateConditionFunctionType = 12
|
||||
)
|
||||
|
||||
type EvaluateConditionEvaluateType int32
|
||||
|
||||
const (
|
||||
EvaluateConditionEvaluateTypeUnknown EvaluateConditionEvaluateType = 0
|
||||
EvaluateConditionEvaluateTypeAnd EvaluateConditionEvaluateType = 1
|
||||
EvaluateConditionEvaluateTypeOr EvaluateConditionEvaluateType = 2
|
||||
EvaluateConditionEvaluateTypeGE EvaluateConditionEvaluateType = 3
|
||||
EvaluateConditionEvaluateTypeIdContain EvaluateConditionEvaluateType = 4
|
||||
EvaluateConditionEvaluateTypeEQ EvaluateConditionEvaluateType = 5
|
||||
EvaluateConditionEvaluateTypeLE EvaluateConditionEvaluateType = 6
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
package model
|
||||
|
||||
const (
|
||||
GachaLabelUnknown int32 = 0
|
||||
GachaLabelPremium int32 = 1
|
||||
GachaLabelEvent int32 = 2
|
||||
GachaLabelChapter int32 = 3
|
||||
GachaLabelPortalCage int32 = 4
|
||||
GachaLabelRecycle int32 = 5
|
||||
)
|
||||
|
||||
const (
|
||||
GachaModeUnknown int32 = 0
|
||||
GachaModeBasic int32 = 1
|
||||
GachaModeStepup int32 = 2
|
||||
GachaModeBox int32 = 3
|
||||
)
|
||||
|
||||
const (
|
||||
GachaUnlockUnknown int32 = 0
|
||||
GachaUnlockNone int32 = 1
|
||||
GachaUnlockUserRankGreaterOrEqual int32 = 2
|
||||
GachaUnlockWithinHoursFromGameStart int32 = 3
|
||||
GachaUnlockMainQuestClear int32 = 4
|
||||
GachaUnlockMainFunctionReleased int32 = 5
|
||||
)
|
||||
|
||||
const (
|
||||
GachaAutoResetUnknown int32 = 0
|
||||
GachaAutoResetNone int32 = 1
|
||||
GachaAutoResetDaily int32 = 2
|
||||
GachaAutoResetMonthly int32 = 3
|
||||
)
|
||||
|
||||
const (
|
||||
GachaDecorationUnknown int32 = 0
|
||||
GachaDecorationNormal int32 = 1
|
||||
GachaDecorationFestival int32 = 2
|
||||
)
|
||||
|
||||
const (
|
||||
GachaBadgeTypeNone int32 = 1
|
||||
)
|
||||
|
||||
const (
|
||||
PriceTypeUnknown int32 = 0
|
||||
PriceTypeConsumableItem int32 = 1
|
||||
PriceTypeGem int32 = 2
|
||||
PriceTypePaidGem int32 = 3
|
||||
PriceTypePlatformPayment int32 = 4
|
||||
)
|
||||
|
||||
const (
|
||||
BannerPrefixLimited = "limited_"
|
||||
BannerPrefixStepUp = "step_up_"
|
||||
BannerPrefixCommon = "common_"
|
||||
)
|
||||
|
||||
func IsMaterialBanner(labelType int32) bool {
|
||||
return labelType == GachaLabelChapter || labelType == GachaLabelRecycle || labelType == GachaLabelPortalCage
|
||||
}
|
||||
|
||||
const MomBannerDomainGacha int32 = 1
|
||||
|
||||
const StepUpGroupDivisor int32 = 1000
|
||||
|
||||
const (
|
||||
PityCeilingCount int32 = 200
|
||||
MedalCountCap int32 = 9999
|
||||
)
|
||||
|
||||
const (
|
||||
PremiumSinglePullPrice int32 = 300
|
||||
PremiumMultiPullPrice int32 = 3000
|
||||
PremiumMultiPullCount int32 = 10
|
||||
)
|
||||
|
||||
const (
|
||||
StepUpStep1Cost int32 = 2000
|
||||
StepUpStep3Cost int32 = 3000
|
||||
StepUpStep5Cost int32 = 5000
|
||||
StepUpFreeCost int32 = 0
|
||||
)
|
||||
|
||||
const (
|
||||
FeaturedRateUpPercent int = 35
|
||||
FeaturedRateUpDenom int = 100
|
||||
)
|
||||
|
||||
const (
|
||||
StepUpRateBoost float64 = 1.5
|
||||
StepUpRateMaxBoost float64 = 2.0
|
||||
)
|
||||
|
||||
const (
|
||||
DupGradeMin int32 = 2
|
||||
DupGradeRange int = 4
|
||||
)
|
||||
|
||||
type DupExchangeEntry struct {
|
||||
PossessionType int32
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
const DefaultDailyDrawLimit int32 = 5
|
||||
|
||||
const (
|
||||
BoxPoolMaxItems int = 50
|
||||
BoxPoolMinItems int = 5
|
||||
BoxItemDefaultMax int32 = 10
|
||||
BoxFallbackItemMax int32 = 20
|
||||
BoxFallbackItemId int32 = 100001
|
||||
)
|
||||
|
||||
const PhaseIdMultiplier int32 = 10
|
||||
|
||||
const (
|
||||
ConsumableIdPremiumTicket int32 = 1
|
||||
ConsumableIdChapterTicket int32 = 2
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
type MaterialType int32
|
||||
|
||||
const (
|
||||
MaterialTypeUnknown MaterialType = 0
|
||||
MaterialTypeWeaponEnhancement MaterialType = 10
|
||||
MaterialTypeCostumeEnhancement MaterialType = 20
|
||||
MaterialTypeCompanionEnhancement MaterialType = 30
|
||||
MaterialTypeWeaponSkillEnhancement MaterialType = 40
|
||||
MaterialTypeCostumeSkillEnhancement MaterialType = 50
|
||||
MaterialTypeCommonSkillEnhancement MaterialType = 60
|
||||
MaterialTypeWeaponEvolution MaterialType = 70
|
||||
MaterialTypeWeaponLimitBreak MaterialType = 80
|
||||
MaterialTypeCostumeLimitBreak MaterialType = 90
|
||||
MaterialTypeCharacterBoardRelease MaterialType = 100
|
||||
MaterialTypeCostumeAwaken MaterialType = 110
|
||||
MaterialTypeCharacterRebirth MaterialType = 120
|
||||
MaterialTypeWeaponAwaken MaterialType = 130
|
||||
MaterialTypeCostumeLotteryEffectUnlock MaterialType = 140
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
type NumericalFunctionType int32
|
||||
|
||||
const (
|
||||
NumericalFunctionTypeUnknown NumericalFunctionType = 0
|
||||
NumericalFunctionTypeLinear NumericalFunctionType = 1
|
||||
NumericalFunctionTypeMonomial NumericalFunctionType = 2
|
||||
NumericalFunctionTypeDuplexLinear NumericalFunctionType = 3
|
||||
NumericalFunctionTypeLinearPermil NumericalFunctionType = 4
|
||||
NumericalFunctionTypePolynomialThird NumericalFunctionType = 5
|
||||
NumericalFunctionTypePolynomialThirdPermil NumericalFunctionType = 6
|
||||
NumericalFunctionTypePartsMainOption NumericalFunctionType = 7
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
type PossessionType int32
|
||||
|
||||
const (
|
||||
PossessionTypeUnknown PossessionType = 0
|
||||
PossessionTypeCostume PossessionType = 1
|
||||
PossessionTypeWeapon PossessionType = 2
|
||||
PossessionTypeCompanion PossessionType = 3
|
||||
PossessionTypeParts PossessionType = 4
|
||||
PossessionTypeMaterial PossessionType = 5
|
||||
PossessionTypeConsumableItem PossessionType = 6
|
||||
PossessionTypeCostumeEnhanced PossessionType = 7
|
||||
PossessionTypeWeaponEnhanced PossessionType = 8
|
||||
PossessionTypeCompanionEnhanced PossessionType = 9
|
||||
PossessionTypePartsEnhanced PossessionType = 10
|
||||
PossessionTypePaidGem PossessionType = 11
|
||||
PossessionTypeFreeGem PossessionType = 12
|
||||
PossessionTypeImportantItem PossessionType = 13
|
||||
PossessionTypeThought PossessionType = 14
|
||||
PossessionTypeMissionPassPoint PossessionType = 15
|
||||
PossessionTypePremiumItem PossessionType = 16
|
||||
)
|
||||
@@ -0,0 +1,160 @@
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type QuestFlowType int32
|
||||
|
||||
const (
|
||||
QuestFlowTypeUnknown QuestFlowType = 0
|
||||
QuestFlowTypeMainFlow QuestFlowType = 1
|
||||
QuestFlowTypeSubFlow QuestFlowType = 2
|
||||
QuestFlowTypeReplayFlow QuestFlowType = 3
|
||||
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
|
||||
)
|
||||
|
||||
func (t QuestFlowType) String() string {
|
||||
switch t {
|
||||
case QuestFlowTypeUnknown:
|
||||
return "unknown"
|
||||
case QuestFlowTypeMainFlow:
|
||||
return "main-flow"
|
||||
case QuestFlowTypeSubFlow:
|
||||
return "sub-flow"
|
||||
case QuestFlowTypeReplayFlow:
|
||||
return "replay-flow"
|
||||
case QuestFlowTypeAnotherRouteReplayFlow:
|
||||
return "another-route-replay-flow"
|
||||
default:
|
||||
return fmt.Sprintf("unknown-quest-flow(%d)", int32(t))
|
||||
}
|
||||
}
|
||||
|
||||
type QuestResultType int32
|
||||
|
||||
const (
|
||||
QuestResultTypeUnknown QuestResultType = 0
|
||||
QuestResultTypeNone QuestResultType = 1
|
||||
QuestResultTypeHalfResult QuestResultType = 2
|
||||
QuestResultTypeFullResult QuestResultType = 3
|
||||
)
|
||||
|
||||
type QuestSceneType int32
|
||||
|
||||
const (
|
||||
QuestSceneTypeUnknown QuestSceneType = 0
|
||||
QuestSceneTypeTower QuestSceneType = 1
|
||||
QuestSceneTypePictureBook QuestSceneType = 2
|
||||
QuestSceneTypeField QuestSceneType = 3
|
||||
QuestSceneTypeNovel QuestSceneType = 4
|
||||
QuestSceneTypeLimitContent QuestSceneType = 5
|
||||
)
|
||||
|
||||
type QuestMissionConditionType int
|
||||
|
||||
const (
|
||||
QuestMissionConditionTypeUnknown QuestMissionConditionType = 0
|
||||
QuestMissionConditionTypeLessThanOrEqualXPeopleNotAlive QuestMissionConditionType = 1
|
||||
QuestMissionConditionTypeMaxDamage QuestMissionConditionType = 2
|
||||
QuestMissionConditionTypeSpecifiedCostumeIsInDeck QuestMissionConditionType = 3
|
||||
QuestMissionConditionTypeSpecifiedCharacterIsInDeck QuestMissionConditionType = 4
|
||||
QuestMissionConditionTypeSpecifiedAttributeMainWeaponIsInDeck QuestMissionConditionType = 5
|
||||
QuestMissionConditionTypeGreaterThanOrEqualXCostumeSkillUseCount QuestMissionConditionType = 6
|
||||
QuestMissionConditionTypeGreaterThanOrEqualXWeaponSkillUseCount QuestMissionConditionType = 7
|
||||
QuestMissionConditionTypeGreaterThanOrEqualXCompanionSkillUseCount QuestMissionConditionType = 8
|
||||
QuestMissionConditionTypeCostumeSkillfulWeaponAllCharacter QuestMissionConditionType = 9
|
||||
QuestMissionConditionTypeCostumeSkillfulWeaponAnyCharacter QuestMissionConditionType = 10
|
||||
QuestMissionConditionTypeCostumeRarityEqAllCharacter QuestMissionConditionType = 11
|
||||
QuestMissionConditionTypeCostumeRarityGeAllCharacter QuestMissionConditionType = 12
|
||||
QuestMissionConditionTypeCostumeRarityLeAllCharacter QuestMissionConditionType = 13
|
||||
QuestMissionConditionTypeCostumeRarityEqAnyCharacter QuestMissionConditionType = 14
|
||||
QuestMissionConditionTypeCostumeRarityGeAnyCharacter QuestMissionConditionType = 15
|
||||
QuestMissionConditionTypeCostumeRarityLeAnyCharacter QuestMissionConditionType = 16
|
||||
QuestMissionConditionTypeWeaponEvolutionGroupId QuestMissionConditionType = 17
|
||||
QuestMissionConditionTypeSpecifiedAttributeWeaponIsInDeck QuestMissionConditionType = 18
|
||||
QuestMissionConditionTypeSpecifiedAttributeMainWeaponAllCharacter QuestMissionConditionType = 19
|
||||
QuestMissionConditionTypeSpecifiedAttributeWeaponAllCharacter QuestMissionConditionType = 20
|
||||
QuestMissionConditionTypeWeaponManSkillfulWeaponAllCharacter QuestMissionConditionType = 21
|
||||
QuestMissionConditionTypeWeaponSkillfulWeaponAllCharacter QuestMissionConditionType = 22
|
||||
QuestMissionConditionTypeWeaponManSkillfulWeaponAnyCharacter QuestMissionConditionType = 23
|
||||
QuestMissionConditionTypeWeaponSkillfulWeaponAnyCharacter QuestMissionConditionType = 24
|
||||
QuestMissionConditionTypeWeaponRarityEqAllCharacter QuestMissionConditionType = 25
|
||||
QuestMissionConditionTypeWeaponRarityGeAllCharacter QuestMissionConditionType = 26
|
||||
QuestMissionConditionTypeWeaponRarityLeAllCharacter QuestMissionConditionType = 27
|
||||
QuestMissionConditionTypeWeaponMainRarityEqAllCharacter QuestMissionConditionType = 28
|
||||
QuestMissionConditionTypeWeaponMainRarityGeAllCharacter QuestMissionConditionType = 29
|
||||
QuestMissionConditionTypeWeaponMainRarityLeAllCharacter QuestMissionConditionType = 30
|
||||
QuestMissionConditionTypeWeaponRarityEqAnyCharacter QuestMissionConditionType = 31
|
||||
QuestMissionConditionTypeWeaponRarityGeAnyCharacter QuestMissionConditionType = 32
|
||||
QuestMissionConditionTypeWeaponRarityLeAnyCharacter QuestMissionConditionType = 33
|
||||
QuestMissionConditionTypeWeaponMainRarityEqAnyCharacter QuestMissionConditionType = 34
|
||||
QuestMissionConditionTypeWeaponMainRarityGeAnyCharacter QuestMissionConditionType = 35
|
||||
QuestMissionConditionTypeWeaponMainRarityLeAnyCharacter QuestMissionConditionType = 36
|
||||
QuestMissionConditionTypeCompanionId QuestMissionConditionType = 37
|
||||
QuestMissionConditionTypeCompanionAttribute QuestMissionConditionType = 38
|
||||
QuestMissionConditionTypeCompanionCategory QuestMissionConditionType = 39
|
||||
QuestMissionConditionTypePartsId QuestMissionConditionType = 40
|
||||
QuestMissionConditionTypePartsGroupId QuestMissionConditionType = 41
|
||||
QuestMissionConditionTypePartsRarityEq QuestMissionConditionType = 42
|
||||
QuestMissionConditionTypePartsRarityGe QuestMissionConditionType = 43
|
||||
QuestMissionConditionTypePartsRarityLe QuestMissionConditionType = 44
|
||||
QuestMissionConditionTypeDeckPowerGe QuestMissionConditionType = 45
|
||||
QuestMissionConditionTypeDeckPowerLe QuestMissionConditionType = 46
|
||||
QuestMissionConditionTypeDeckCostumeNumEq QuestMissionConditionType = 47
|
||||
QuestMissionConditionTypeDeckCostumeNumGe QuestMissionConditionType = 48
|
||||
QuestMissionConditionTypeDeckCostumeNumLe QuestMissionConditionType = 49
|
||||
QuestMissionConditionTypeCriticalCountGe QuestMissionConditionType = 50
|
||||
QuestMissionConditionTypeMinHpPercentageGe QuestMissionConditionType = 51
|
||||
QuestMissionConditionTypeComboCountGe QuestMissionConditionType = 52
|
||||
QuestMissionConditionTypeComboMaxDamageGe QuestMissionConditionType = 53
|
||||
QuestMissionConditionTypeLessThanOrEqualXCostumeSkillUseCount QuestMissionConditionType = 54
|
||||
QuestMissionConditionTypeLessThanOrEqualXWeaponSkillUseCount QuestMissionConditionType = 55
|
||||
QuestMissionConditionTypeLessThanOrEqualXCompanionSkillUseCount QuestMissionConditionType = 56
|
||||
QuestMissionConditionTypeWithoutRecoverySkill QuestMissionConditionType = 57
|
||||
QuestMissionConditionTypeWithoutCostumeSkill QuestMissionConditionType = 58
|
||||
QuestMissionConditionTypeWithoutWeaponSkill QuestMissionConditionType = 59
|
||||
QuestMissionConditionTypeWithoutCompanionSkill QuestMissionConditionType = 60
|
||||
QuestMissionConditionTypeCharacterContainAll QuestMissionConditionType = 61
|
||||
QuestMissionConditionTypeCharacterContainAny QuestMissionConditionType = 62
|
||||
QuestMissionConditionTypeCostumeContainAll QuestMissionConditionType = 63
|
||||
QuestMissionConditionTypeCostumeContainAny QuestMissionConditionType = 64
|
||||
QuestMissionConditionTypeCostumeSkillfulWeaponContainAll QuestMissionConditionType = 65
|
||||
QuestMissionConditionTypeCostumeSkillfulWeaponContainAny QuestMissionConditionType = 66
|
||||
QuestMissionConditionTypeAttributeMainWeaponContainAll QuestMissionConditionType = 67
|
||||
QuestMissionConditionTypeAttributeMainWeaponContainAny QuestMissionConditionType = 68
|
||||
QuestMissionConditionTypeAttributeWeaponContainAll QuestMissionConditionType = 69
|
||||
QuestMissionConditionTypeAttributeWeaponContainAny QuestMissionConditionType = 70
|
||||
QuestMissionConditionTypeWeaponManSkillfulWeaponContainAll QuestMissionConditionType = 71
|
||||
QuestMissionConditionTypeWeaponManSkillfulWeaponContainAny QuestMissionConditionType = 72
|
||||
QuestMissionConditionTypeWeaponSkillfulWeaponContainAll QuestMissionConditionType = 73
|
||||
QuestMissionConditionTypeWeaponSkillfulWeaponContainAny QuestMissionConditionType = 74
|
||||
QuestMissionConditionTypeComplete QuestMissionConditionType = 9999
|
||||
)
|
||||
|
||||
type WeaponStoryReleaseConditionType int32
|
||||
|
||||
const (
|
||||
WeaponStoryReleaseConditionTypeUnknown WeaponStoryReleaseConditionType = 0
|
||||
WeaponStoryReleaseConditionTypeAcquisition WeaponStoryReleaseConditionType = 1
|
||||
WeaponStoryReleaseConditionTypeReachSpecifiedLevel WeaponStoryReleaseConditionType = 2
|
||||
WeaponStoryReleaseConditionTypeReachInitialMaxLevel WeaponStoryReleaseConditionType = 3
|
||||
WeaponStoryReleaseConditionTypeReachOnceEvolvedMaxLevel WeaponStoryReleaseConditionType = 4
|
||||
WeaponStoryReleaseConditionTypeReachSpecifiedEvolutionCount WeaponStoryReleaseConditionType = 5
|
||||
WeaponStoryReleaseConditionTypeQuestClear WeaponStoryReleaseConditionType = 6
|
||||
WeaponStoryReleaseConditionTypeMainFlowSceneProgress WeaponStoryReleaseConditionType = 7
|
||||
)
|
||||
|
||||
type UserQuestStateType int32
|
||||
|
||||
const (
|
||||
UserQuestStateTypeUnknown UserQuestStateType = 0
|
||||
UserQuestStateTypeActive UserQuestStateType = 1
|
||||
UserQuestStateTypeCleared UserQuestStateType = 2
|
||||
)
|
||||
|
||||
type SideStoryQuestStateType int32
|
||||
|
||||
const (
|
||||
SideStoryQuestStateUnknown SideStoryQuestStateType = 0
|
||||
SideStoryQuestStateActive SideStoryQuestStateType = 1
|
||||
SideStoryQuestStateCleared SideStoryQuestStateType = 2
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
type RarityType = int32
|
||||
|
||||
const (
|
||||
RarityNormal RarityType = 10
|
||||
RarityRare RarityType = 20
|
||||
RaritySRare RarityType = 30
|
||||
RaritySSRare RarityType = 40
|
||||
RarityLegend RarityType = 50
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
const (
|
||||
ShopGroupTypeUnknown int32 = 0
|
||||
ShopGroupTypePremiumShop int32 = 1
|
||||
ShopGroupTypeItemShop int32 = 3
|
||||
ShopGroupTypeExchangeShop int32 = 4
|
||||
ShopGroupTypeRecoveryShop int32 = 5
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
type StatusKindType int32
|
||||
|
||||
const (
|
||||
StatusKindTypeUnknown StatusKindType = 0
|
||||
StatusKindTypeAgility StatusKindType = 1
|
||||
StatusKindTypeAttack StatusKindType = 2
|
||||
StatusKindTypeCriticalAttack StatusKindType = 3
|
||||
StatusKindTypeCriticalRatio StatusKindType = 4
|
||||
StatusKindTypeEvasionRatio StatusKindType = 5
|
||||
StatusKindTypeHp StatusKindType = 6
|
||||
StatusKindTypeVitality StatusKindType = 7
|
||||
)
|
||||
|
||||
type CostumeAwakenEffectType int32
|
||||
|
||||
const (
|
||||
CostumeAwakenEffectTypeUnknown CostumeAwakenEffectType = 0
|
||||
CostumeAwakenEffectTypeStatusUp CostumeAwakenEffectType = 1
|
||||
CostumeAwakenEffectTypeAbility CostumeAwakenEffectType = 2
|
||||
CostumeAwakenEffectTypeItemAcquire CostumeAwakenEffectType = 3
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
package model
|
||||
|
||||
type TutorialType int32
|
||||
|
||||
const (
|
||||
TutorialTypeUnknown TutorialType = 0
|
||||
TutorialTypeGameStart TutorialType = 1
|
||||
TutorialTypeMenuFirst TutorialType = 2
|
||||
TutorialTypeMenuSecond TutorialType = 3
|
||||
TutorialTypeBattleWeaponSkill TutorialType = 4
|
||||
TutorialTypeBattleCostumeSkill TutorialType = 5
|
||||
TutorialTypeBlackBird TutorialType = 6
|
||||
TutorialTypeEnhance TutorialType = 7
|
||||
TutorialTypeCompanion TutorialType = 8
|
||||
TutorialTypeParts TutorialType = 9
|
||||
TutorialTypeExplore TutorialType = 10
|
||||
TutorialTypePvp TutorialType = 11
|
||||
TutorialTypeMainQuestHard TutorialType = 12
|
||||
TutorialTypeMainQuestVeryHard TutorialType = 13
|
||||
TutorialTypeEventQuestFirst TutorialType = 14
|
||||
TutorialTypeEventQuestCharacter TutorialType = 15
|
||||
TutorialTypeEventQuestMarathon TutorialType = 16
|
||||
TutorialTypeEventQuestHunt TutorialType = 17
|
||||
TutorialTypeEventQuestDungeon TutorialType = 18
|
||||
TutorialTypeEventQuestDayOfTheWeek TutorialType = 19
|
||||
TutorialTypeEventQuestGuerrilla TutorialType = 20
|
||||
TutorialTypeEndContents TutorialType = 21
|
||||
TutorialTypeEndContentsQuest TutorialType = 22
|
||||
TutorialTypeExploreGame1 TutorialType = 23
|
||||
TutorialTypeExploreGame2 TutorialType = 24
|
||||
TutorialTypePortalCage TutorialType = 25
|
||||
TutorialTypePortalCageMainQuest TutorialType = 26
|
||||
TutorialTypeCage TutorialType = 27
|
||||
TutorialTypePortalCageDailyQuest TutorialType = 28
|
||||
TutorialTypePortalCageDailyGacha TutorialType = 29
|
||||
TutorialTypePortalCageDropItem TutorialType = 30
|
||||
TutorialTypePortalCageReachedLastScene TutorialType = 31
|
||||
TutorialTypePortalCageCharacter1 TutorialType = 32
|
||||
TutorialTypePortalCageCharacter2 TutorialType = 33
|
||||
TutorialTypePortalCageCharacter3 TutorialType = 34
|
||||
TutorialTypePortalCageCharacter4 TutorialType = 35
|
||||
TutorialTypePortalCageCharacter5 TutorialType = 36
|
||||
TutorialTypeBlackBirdCharacter1 TutorialType = 37
|
||||
TutorialTypeBlackBirdCharacter2 TutorialType = 38
|
||||
TutorialTypeBlackBirdCharacter3 TutorialType = 39
|
||||
TutorialTypeGohobi TutorialType = 40
|
||||
TutorialTypeGohobiDrop TutorialType = 41
|
||||
TutorialTypeBattleCancelEnemyCast1 TutorialType = 42
|
||||
TutorialTypeBattleCancelEnemyCast2 TutorialType = 43
|
||||
TutorialTypeLoseFirst TutorialType = 44
|
||||
TutorialTypeRewardGacha TutorialType = 45
|
||||
TutorialTypeBigWinBonusFirst TutorialType = 46
|
||||
TutorialTypeBigHunt TutorialType = 47
|
||||
TutorialTypeTripleDeck TutorialType = 48
|
||||
TutorialTypeCharacterBoard TutorialType = 49
|
||||
TutorialTypeCharacterBoardBasic TutorialType = 50
|
||||
TutorialTypeCharacterBoardBigHunt TutorialType = 51
|
||||
TutorialTypeWorldMap TutorialType = 52
|
||||
TutorialTypeMapItemFull TutorialType = 53
|
||||
TutorialTypeWorldMapBlackBird TutorialType = 54
|
||||
TutorialTypeWorldMapTreasure TutorialType = 55
|
||||
TutorialTypeBrokenObelisk TutorialType = 56
|
||||
TutorialTypeLoseFirstAfterChapter TutorialType = 57
|
||||
TutorialTypeReplayFlowSkip TutorialType = 58
|
||||
TutorialTypeWorldMapOutgame TutorialType = 59
|
||||
TutorialTypeBattleCertainKillSkill TutorialType = 60
|
||||
TutorialTypeSmartPhoneFirst TutorialType = 101
|
||||
TutorialTypePhotoFirst TutorialType = 102
|
||||
TutorialTypeDailyGacha TutorialType = 103
|
||||
TutorialTypePortalCageSeason TutorialType = 104
|
||||
TutorialTypeQuestSkip TutorialType = 201
|
||||
TutorialTypePortalCageChapter TutorialType = 202
|
||||
TutorialTypeCharacterBoardUnlock TutorialType = 301
|
||||
TutorialTypeBlackBirdSistersFirst TutorialType = 401
|
||||
TutorialTypeCostumeLevelBonus TutorialType = 501
|
||||
TutorialTypeWorldMapReport TutorialType = 601
|
||||
TutorialTypeBossSpecialEffect TutorialType = 701
|
||||
TutorialTypeEventQuestGuerrillaFree TutorialType = 801
|
||||
TutorialTypeExploreHard TutorialType = 901
|
||||
TutorialTypeCageMemory TutorialType = 1001
|
||||
TutorialTypeDressupCostume TutorialType = 1101
|
||||
TutorialTypeCostumeAwaken TutorialType = 1201
|
||||
TutorialTypeThoughtOrganization TutorialType = 1202
|
||||
TutorialTypeHideObelisk TutorialType = 1301
|
||||
TutorialTypeLimitContent TutorialType = 1302
|
||||
TutorialTypeFieldEffect TutorialType = 1303
|
||||
TutorialTypeLimitContentCage TutorialType = 1304
|
||||
TutorialTypeCharacterViewer TutorialType = 1305
|
||||
TutorialTypeRecycleGacha TutorialType = 1306
|
||||
TutorialTypeMomPoint TutorialType = 1401
|
||||
TutorialTypeStainedGlass TutorialType = 1402
|
||||
TutorialTypeCharacterRebirth TutorialType = 1501
|
||||
TutorialTypeWeaponAwaken TutorialType = 1502
|
||||
TutorialTypeEventQuestLabyrinth TutorialType = 1503
|
||||
TutorialTypeProperAttribute TutorialType = 1601
|
||||
TutorialTypeMissionPass TutorialType = 1701
|
||||
TutorialTypeWeaponAllOrganization TutorialType = 1702
|
||||
TutorialTypeCostumeLotteryEffect TutorialType = 1801
|
||||
TutorialTypeAnotherRoute TutorialType = 2001
|
||||
TutorialTypeDeleteCostumeFio TutorialType = 2101
|
||||
)
|
||||
|
||||
type TutorialUnlockConditionType int32
|
||||
|
||||
const (
|
||||
TutorialUnlockConditionTypeFunctionReleased TutorialUnlockConditionType = 1
|
||||
TutorialUnlockConditionTypeReachSpecifiedQuestScene TutorialUnlockConditionType = 2
|
||||
TutorialUnlockConditionTypeUntilReachSpecifiedScene TutorialUnlockConditionType = 3
|
||||
)
|
||||
|
||||
// TutorialPhase values are runtime-initialized in the client (static readonly),
|
||||
// so only observed values are listed here.
|
||||
type TutorialPhase int32
|
||||
|
||||
const (
|
||||
TutorialPhaseMomMenuEditDeck TutorialPhase = 20
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) HandleBigHuntQuestStart(user *store.UserState, questId, userDeckNumber int32, nowMillis int64) {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestStart", questId))
|
||||
}
|
||||
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
if quest.Stamina > 0 {
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
questState := user.Quests[questId]
|
||||
questState.UserDeckNumber = userDeckNumber
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, outcome, nowMillis)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
refund := quest.Stamina - 1
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
h.clearQuestMissions(user, questId, nowMillis)
|
||||
|
||||
return outcome
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestStart(user *store.UserState, eventQuestChapterId, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestStart", questId))
|
||||
}
|
||||
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
if quest.Stamina > 0 {
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
questState := user.Quests[questId]
|
||||
questState.IsBattleOnly = isBattleOnly
|
||||
questState.UserDeckNumber = userDeckNumber
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
|
||||
user.EventQuest.CurrentEventQuestChapterId = eventQuestChapterId
|
||||
user.EventQuest.CurrentQuestId = questId
|
||||
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||
user.EventQuest.CurrentQuestSceneId = sceneIds[0]
|
||||
user.EventQuest.HeadQuestSceneId = sceneIds[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestChapterId, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, outcome, nowMillis)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
refund := quest.Stamina - 1
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
user.EventQuest.CurrentQuestId = 0
|
||||
user.EventQuest.CurrentQuestSceneId = 0
|
||||
user.EventQuest.HeadQuestSceneId = 0
|
||||
|
||||
h.clearQuestMissions(user, questId, nowMillis)
|
||||
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
|
||||
h.HandleQuestRestart(user, questId, nowMillis)
|
||||
|
||||
user.EventQuest.CurrentEventQuestChapterId = eventQuestChapterId
|
||||
user.EventQuest.CurrentQuestId = questId
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||
return
|
||||
}
|
||||
|
||||
user.EventQuest.CurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.EventQuest.HeadQuestSceneId) {
|
||||
user.EventQuest.HeadQuestSceneId = questSceneId
|
||||
}
|
||||
|
||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||
|
||||
if scene.QuestResultType == model.QuestResultTypeHalfResult {
|
||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) HandleExtraQuestStart(user *store.UserState, questId, userDeckNumber int32, nowMillis int64) {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestStart", questId))
|
||||
}
|
||||
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
if quest.Stamina > 0 {
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
questState := user.Quests[questId]
|
||||
questState.UserDeckNumber = userDeckNumber
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
|
||||
user.ExtraQuest.CurrentQuestId = questId
|
||||
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||
user.ExtraQuest.CurrentQuestSceneId = sceneIds[0]
|
||||
user.ExtraQuest.HeadQuestSceneId = sceneIds[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, outcome, nowMillis)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
refund := quest.Stamina - 1
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
user.ExtraQuest.CurrentQuestId = 0
|
||||
user.ExtraQuest.CurrentQuestSceneId = 0
|
||||
user.ExtraQuest.HeadQuestSceneId = 0
|
||||
|
||||
h.clearQuestMissions(user, questId, nowMillis)
|
||||
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleExtraQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
||||
h.HandleQuestRestart(user, questId, nowMillis)
|
||||
|
||||
user.ExtraQuest.CurrentQuestId = questId
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||
return
|
||||
}
|
||||
|
||||
user.ExtraQuest.CurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.ExtraQuest.HeadQuestSceneId) {
|
||||
user.ExtraQuest.HeadQuestSceneId = questSceneId
|
||||
}
|
||||
|
||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||
|
||||
if scene.QuestResultType == model.QuestResultTypeHalfResult {
|
||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
type RewardGrant struct {
|
||||
PossessionType model.PossessionType
|
||||
PossessionId int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
type FinishOutcome struct {
|
||||
DropRewards []RewardGrant
|
||||
FirstClearRewards []RewardGrant
|
||||
ReplayFlowFirstClearRewards []RewardGrant
|
||||
MissionClearRewards []RewardGrant
|
||||
MissionClearCompleteRewards []RewardGrant
|
||||
BigWinClearedQuestMissionIds []int32
|
||||
IsBigWin bool
|
||||
}
|
||||
|
||||
type QuestHandler struct {
|
||||
*masterdata.QuestCatalog
|
||||
Config *masterdata.GameConfig
|
||||
Granter *store.PossessionGranter
|
||||
}
|
||||
|
||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler {
|
||||
granter := BuildGranter(catalog)
|
||||
return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter}
|
||||
}
|
||||
|
||||
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
||||
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
|
||||
for id, cm := range catalog.CostumeById {
|
||||
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
|
||||
}
|
||||
weaponById := make(map[int32]store.WeaponRef, len(catalog.WeaponById))
|
||||
for id, wm := range catalog.WeaponById {
|
||||
weaponById[id] = store.WeaponRef{
|
||||
WeaponSkillGroupId: wm.WeaponSkillGroupId,
|
||||
WeaponAbilityGroupId: wm.WeaponAbilityGroupId,
|
||||
WeaponStoryReleaseConditionGroupId: wm.WeaponStoryReleaseConditionGroupId,
|
||||
}
|
||||
}
|
||||
releaseConditions := make(map[int32][]store.WeaponStoryReleaseCond, len(catalog.ReleaseConditionsByGroupId))
|
||||
for groupId, rows := range catalog.ReleaseConditionsByGroupId {
|
||||
conds := make([]store.WeaponStoryReleaseCond, len(rows))
|
||||
for i, r := range rows {
|
||||
conds[i] = store.WeaponStoryReleaseCond{
|
||||
StoryIndex: r.StoryIndex,
|
||||
WeaponStoryReleaseConditionType: r.WeaponStoryReleaseConditionType,
|
||||
ConditionValue: r.ConditionValue,
|
||||
}
|
||||
}
|
||||
releaseConditions[groupId] = conds
|
||||
}
|
||||
return &store.PossessionGranter{
|
||||
CostumeById: costumeById,
|
||||
WeaponById: weaponById,
|
||||
WeaponSkillSlots: catalog.WeaponSkillSlots,
|
||||
WeaponAbilitySlots: catalog.WeaponAbilitySlots,
|
||||
ReleaseConditions: releaseConditions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) initQuestState(user *store.UserState, questId int32) {
|
||||
quest := user.Quests[questId]
|
||||
quest.QuestId = questId
|
||||
user.Quests[questId] = quest
|
||||
|
||||
for _, missionId := range h.MissionIdsByQuestId[questId] {
|
||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
|
||||
mission := user.QuestMissions[key]
|
||||
mission.QuestId = questId
|
||||
mission.QuestMissionId = missionId
|
||||
user.QuestMissions[key] = mission
|
||||
}
|
||||
}
|
||||
|
||||
func isMainQuestPlayable(quest masterdata.QuestRow) bool {
|
||||
return !quest.IsRunInTheBackground && quest.IsCountedAsQuest
|
||||
}
|
||||
|
||||
func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32, nowMillis int64) {
|
||||
for _, missionId := range h.MissionIdsByQuestId[questId] {
|
||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
|
||||
mission := user.QuestMissions[key]
|
||||
mission.IsClear = true
|
||||
mission.ProgressValue = 1
|
||||
mission.LatestClearDatetime = nowMillis
|
||||
user.QuestMissions[key] = mission
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, false, nowMillis)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, true, nowMillis)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
|
||||
}
|
||||
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
if quest.Stamina > 0 {
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
questState := user.Quests[questId]
|
||||
if questState.QuestStateType == model.UserQuestStateTypeCleared {
|
||||
if isReplayFlow {
|
||||
user.MainQuest.SavedCurrentQuestSceneId = user.MainQuest.CurrentQuestSceneId
|
||||
user.MainQuest.SavedHeadQuestSceneId = user.MainQuest.HeadQuestSceneId
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
||||
user.MainQuest.LatestVersion = nowMillis
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
questState.IsBattleOnly = isBattleOnly
|
||||
questState.UserDeckNumber = userDeckNumber
|
||||
user.Quests[questId] = questState
|
||||
log.Printf("[HandleQuestStart] replay flow started for quest %d, saved scene=%d head=%d",
|
||||
questId, user.MainQuest.SavedCurrentQuestSceneId, user.MainQuest.SavedHeadQuestSceneId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
questState.IsBattleOnly = isBattleOnly
|
||||
questState.UserDeckNumber = userDeckNumber
|
||||
if isMainQuestPlayable(quest) {
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
} else {
|
||||
questState.QuestStateType = model.UserQuestStateTypeCleared
|
||||
questState.ClearCount = 1
|
||||
questState.DailyClearCount = 1
|
||||
questState.LastClearDatetime = nowMillis
|
||||
|
||||
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||
firstSceneId := sceneIds[0]
|
||||
prevSceneId := user.MainQuest.CurrentQuestSceneId
|
||||
user.MainQuest.CurrentQuestSceneId = firstSceneId
|
||||
if h.isSceneAhead(firstSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||
user.MainQuest.HeadQuestSceneId = firstSceneId
|
||||
}
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
lastSceneId := h.getChapterLastSceneId(questId)
|
||||
user.MainQuest.IsReachedLastQuestScene = firstSceneId == lastSceneId
|
||||
if routeId, ok := h.RouteIdByQuestId[questId]; ok {
|
||||
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
|
||||
user.MainQuest.MainQuestSeasonId = seasonId
|
||||
}
|
||||
}
|
||||
log.Printf("[HandleQuestStart] background quest %d auto-cleared, scene %d -> %d", questId, prevSceneId, firstSceneId)
|
||||
}
|
||||
}
|
||||
user.Quests[questId] = questState
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome FinishOutcome, nowMillis int64) {
|
||||
questState := user.Quests[questId]
|
||||
if !questState.IsRewardGranted {
|
||||
h.applyQuestRewards(user, questId, nowMillis)
|
||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)
|
||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)
|
||||
questState.IsRewardGranted = true
|
||||
}
|
||||
for _, drop := range outcome.DropRewards {
|
||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
||||
}
|
||||
for _, reward := range outcome.ReplayFlowFirstClearRewards {
|
||||
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
|
||||
}
|
||||
questState.QuestStateType = model.UserQuestStateTypeCleared
|
||||
questState.ClearCount++
|
||||
questState.DailyClearCount++
|
||||
questState.LastClearDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||
quest, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
|
||||
}
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, outcome, nowMillis)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
refund := quest.Stamina - 1
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||
}
|
||||
|
||||
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
|
||||
|
||||
user.MainQuest.ProgressQuestSceneId = 0
|
||||
user.MainQuest.ProgressHeadQuestSceneId = 0
|
||||
user.MainQuest.ProgressQuestFlowType = 0
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
|
||||
|
||||
if wasReplay {
|
||||
if user.MainQuest.SavedCurrentQuestSceneId > 0 {
|
||||
user.MainQuest.CurrentQuestSceneId = user.MainQuest.SavedCurrentQuestSceneId
|
||||
}
|
||||
if user.MainQuest.SavedHeadQuestSceneId > 0 {
|
||||
user.MainQuest.HeadQuestSceneId = user.MainQuest.SavedHeadQuestSceneId
|
||||
}
|
||||
user.MainQuest.SavedCurrentQuestSceneId = 0
|
||||
user.MainQuest.SavedHeadQuestSceneId = 0
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
||||
log.Printf("[HandleQuestFinish] replay flow ended for quest %d, restored scene=%d head=%d",
|
||||
questId, user.MainQuest.CurrentQuestSceneId, user.MainQuest.HeadQuestSceneId)
|
||||
}
|
||||
|
||||
h.clearQuestMissions(user, questId, nowMillis)
|
||||
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount int32, nowMillis int64) FinishOutcome {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
|
||||
}
|
||||
|
||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||
store.ConsumeStamina(user, skipCount, maxMillis, nowMillis)
|
||||
|
||||
skipTicketId := h.Config.ConsumableItemIdForQuestSkipTicket
|
||||
user.ConsumableItems[skipTicketId] -= skipCount
|
||||
if user.ConsumableItems[skipTicketId] < 0 {
|
||||
user.ConsumableItems[skipTicketId] = 0
|
||||
}
|
||||
|
||||
var allDrops []RewardGrant
|
||||
for range skipCount {
|
||||
drops := h.computeDropRewards(questDef)
|
||||
for _, drop := range drops {
|
||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
||||
}
|
||||
allDrops = append(allDrops, drops...)
|
||||
|
||||
if questDef.Gold != 0 {
|
||||
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
||||
}
|
||||
h.applyExpRewards(user, questId, nowMillis)
|
||||
}
|
||||
|
||||
questState := user.Quests[questId]
|
||||
questState.ClearCount += skipCount
|
||||
questState.DailyClearCount += skipCount
|
||||
questState.LastClearDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
|
||||
log.Printf("[HandleQuestSkip] questId=%d skipCount=%d drops=%d gold=%d", questId, skipCount, len(allDrops), questDef.Gold*skipCount)
|
||||
return FinishOutcome{DropRewards: allDrops}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if ok && isMainQuestPlayable(questDef) {
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
}
|
||||
|
||||
quest := user.Quests[questId]
|
||||
quest.QuestId = questId
|
||||
quest.QuestStateType = model.UserQuestStateTypeActive
|
||||
quest.IsBattleOnly = false
|
||||
quest.LatestStartDatetime = nowMillis
|
||||
user.Quests[questId] = quest
|
||||
|
||||
for _, missionId := range h.MissionIdsByQuestId[questId] {
|
||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: missionId}
|
||||
m := user.QuestMissions[key]
|
||||
m.QuestId = questId
|
||||
m.QuestMissionId = missionId
|
||||
m.IsClear = false
|
||||
m.ProgressValue = 0
|
||||
m.LatestClearDatetime = 0
|
||||
user.QuestMissions[key] = m
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) isQuestCleared(user *store.UserState, questId int32) bool {
|
||||
quest, ok := user.Quests[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for isQuestCleared", questId))
|
||||
}
|
||||
return quest.QuestStateType == model.UserQuestStateTypeCleared
|
||||
}
|
||||
|
||||
func appendMissionRewards(dst []RewardGrant, src []masterdata.QuestMissionRewardRow) []RewardGrant {
|
||||
for _, r := range src {
|
||||
dst = append(dst, RewardGrant{
|
||||
PossessionType: r.PossessionType,
|
||||
PossessionId: r.PossessionId,
|
||||
Count: r.Count,
|
||||
})
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef masterdata.QuestRow) int32 {
|
||||
rewardGroupId := questDef.QuestFirstClearRewardGroupId
|
||||
for _, switchRow := range h.FirstClearRewardSwitchesByQuestId[questDef.QuestId] {
|
||||
if h.isQuestCleared(user, switchRow.SwitchConditionClearQuestId) {
|
||||
rewardGroupId = switchRow.QuestFirstClearRewardGroupId
|
||||
break
|
||||
}
|
||||
}
|
||||
return rewardGroupId
|
||||
}
|
||||
|
||||
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32) FinishOutcome {
|
||||
outcome := FinishOutcome{}
|
||||
questState, ok := user.Quests[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
||||
}
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
||||
}
|
||||
|
||||
if !questState.IsRewardGranted {
|
||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
|
||||
PossessionType: reward.PossessionType,
|
||||
PossessionId: reward.PossessionId,
|
||||
Count: reward.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow) && questDef.QuestReplayFlowRewardGroupId > 0 {
|
||||
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
|
||||
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
||||
PossessionType: reward.PossessionType,
|
||||
PossessionId: reward.PossessionId,
|
||||
Count: reward.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pendingClearCount := 0
|
||||
regularMissionCount := 0
|
||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||
missionDef, ok := h.MissionById[questMissionId]
|
||||
if !ok || missionDef.QuestMissionConditionType == model.QuestMissionConditionTypeComplete {
|
||||
continue
|
||||
}
|
||||
regularMissionCount++
|
||||
|
||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||
mission := user.QuestMissions[key]
|
||||
|
||||
if !mission.IsClear {
|
||||
pendingClearCount++
|
||||
outcome.MissionClearRewards = appendMissionRewards(
|
||||
outcome.MissionClearRewards,
|
||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
priorClearCount := regularMissionCount - pendingClearCount
|
||||
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
|
||||
// always equals regularMissionCount. The two-variable form is kept to mirror the
|
||||
// original game's intent where individual missions could fail their conditions.
|
||||
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
|
||||
if allRegularWillClear {
|
||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||
missionDef, ok := h.MissionById[questMissionId]
|
||||
if !ok || missionDef.QuestMissionConditionType != model.QuestMissionConditionTypeComplete {
|
||||
continue
|
||||
}
|
||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||
if !user.QuestMissions[key].IsClear {
|
||||
outcome.MissionClearCompleteRewards = appendMissionRewards(
|
||||
outcome.MissionClearCompleteRewards,
|
||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||
)
|
||||
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
|
||||
}
|
||||
}
|
||||
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
||||
}
|
||||
|
||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (h *QuestHandler) computeDropRewards(questDef masterdata.QuestRow) []RewardGrant {
|
||||
if questDef.QuestPickupRewardGroupId == 0 {
|
||||
return nil
|
||||
}
|
||||
var drops []RewardGrant
|
||||
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
|
||||
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
|
||||
drops = append(drops, RewardGrant{
|
||||
PossessionType: bdr.PossessionType,
|
||||
PossessionId: bdr.PossessionId,
|
||||
Count: bdr.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
return drops
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
oldLevel := user.Status.Level
|
||||
user.Status.Exp += questDef.UserExp
|
||||
user.Status.Level, user.Status.Exp = gameutil.LevelAndCap(user.Status.Exp, h.UserExpThresholds)
|
||||
log.Printf("[applyExpRewards] questId=%d user: +%d exp -> total=%d level=%d", questId, questDef.UserExp, user.Status.Exp, user.Status.Level)
|
||||
|
||||
if user.Status.Level > oldLevel {
|
||||
if maxStamina, ok := h.MaxStaminaByLevel[user.Status.Level]; ok {
|
||||
store.ReplenishStamina(user, maxStamina*1000, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
if h.RentalQuestIds[questId] {
|
||||
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (rental deck)", questId)
|
||||
return
|
||||
}
|
||||
|
||||
deckCostumeUuids, deckCharacterIds := h.resolveDeckUnits(user, questId)
|
||||
if deckCostumeUuids == nil {
|
||||
log.Printf("[applyExpRewards] questId=%d skipping character/costume exp (deck not resolved)", questId)
|
||||
return
|
||||
}
|
||||
|
||||
if questDef.CharacterExp != 0 {
|
||||
for id := range deckCharacterIds {
|
||||
row := user.Characters[id]
|
||||
row.Exp += questDef.CharacterExp
|
||||
row.Level, row.Exp = gameutil.LevelAndCap(row.Exp, h.CharacterExpThresholds)
|
||||
user.Characters[id] = row
|
||||
log.Printf("[applyExpRewards] questId=%d character=%d: +%d exp -> total=%d level=%d", questId, id, questDef.CharacterExp, row.Exp, row.Level)
|
||||
}
|
||||
}
|
||||
|
||||
if questDef.CostumeExp != 0 {
|
||||
for key := range deckCostumeUuids {
|
||||
row := user.Costumes[key]
|
||||
cm, ok := h.CostumeById[row.CostumeId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
||||
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
|
||||
if row.Level >= maxLevel {
|
||||
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): at max level %d, skipping", questId, row.CostumeId, key, row.Level)
|
||||
continue
|
||||
}
|
||||
}
|
||||
row.Exp += questDef.CostumeExp
|
||||
if thresholds, ok := h.CostumeExpByRarity[cm.RarityType]; ok {
|
||||
row.Level, row.Exp = gameutil.LevelAndCap(row.Exp, thresholds)
|
||||
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
||||
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
|
||||
if row.Level > maxLevel && int(maxLevel) < len(thresholds) {
|
||||
row.Level = maxLevel
|
||||
row.Exp = thresholds[maxLevel]
|
||||
}
|
||||
}
|
||||
}
|
||||
user.Costumes[key] = row
|
||||
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): +%d exp -> total=%d level=%d", questId, row.CostumeId, key, questDef.CostumeExp, row.Exp, row.Level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) resolveDeckUnits(user *store.UserState, questId int32) (costumeUuids map[string]bool, characterIds map[int32]bool) {
|
||||
dn := user.Quests[questId].UserDeckNumber
|
||||
if dn == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
deck, ok := user.Decks[store.DeckKey{DeckType: model.DeckTypeQuest, UserDeckNumber: dn}]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
costumeUuids = make(map[string]bool)
|
||||
characterIds = make(map[int32]bool)
|
||||
for _, dcUuid := range []string{deck.UserDeckCharacterUuid01, deck.UserDeckCharacterUuid02, deck.UserDeckCharacterUuid03} {
|
||||
if dcUuid == "" {
|
||||
continue
|
||||
}
|
||||
dc, ok := user.DeckCharacters[dcUuid]
|
||||
if !ok || dc.UserCostumeUuid == "" {
|
||||
continue
|
||||
}
|
||||
costumeUuids[dc.UserCostumeUuid] = true
|
||||
if costume, ok := user.Costumes[dc.UserCostumeUuid]; ok {
|
||||
if cm, ok := h.CostumeById[costume.CostumeId]; ok {
|
||||
characterIds[cm.CharacterId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(costumeUuids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return costumeUuids, characterIds
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
h.applyExpRewards(user, questId, nowMillis)
|
||||
|
||||
if questDef.Gold != 0 {
|
||||
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
||||
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
|
||||
}
|
||||
|
||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
|
||||
switch possType {
|
||||
case model.PossessionTypeCompanion:
|
||||
h.grantCompanion(user, possId, nowMillis)
|
||||
case model.PossessionTypeParts:
|
||||
h.grantParts(user, possId, nowMillis)
|
||||
default:
|
||||
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) grantCompanion(user *store.UserState, companionId int32, nowMillis int64) {
|
||||
for _, row := range user.Companions {
|
||||
if row.CompanionId == companionId {
|
||||
return
|
||||
}
|
||||
}
|
||||
key := fmt.Sprintf("reward-companion-%d", companionId)
|
||||
user.Companions[key] = store.CompanionState{
|
||||
UserCompanionUuid: key,
|
||||
CompanionId: companionId,
|
||||
Level: 1,
|
||||
HeadupDisplayViewId: 1,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) grantParts(user *store.UserState, partsId int32, nowMillis int64) {
|
||||
for _, row := range user.Parts {
|
||||
if row.PartsId == partsId {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var mainStatId int32
|
||||
if partsDef, ok := h.PartsById[partsId]; ok {
|
||||
mainStatId = h.DefaultPartsStatusMainByLotteryGroup[partsDef.PartsStatusMainLotteryGroupId]
|
||||
|
||||
if _, exists := user.PartsGroupNotes[partsDef.PartsGroupId]; !exists {
|
||||
user.PartsGroupNotes[partsDef.PartsGroupId] = store.PartsGroupNoteState{
|
||||
PartsGroupId: partsDef.PartsGroupId,
|
||||
FirstAcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("reward-parts-%d", partsId)
|
||||
user.Parts[key] = store.PartsState{
|
||||
UserPartsUuid: key,
|
||||
PartsId: partsId,
|
||||
Level: 1,
|
||||
PartsStatusMainId: mainStatId,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) grantWeaponStoryUnlock(user *store.UserState, weaponId, storyIndex int32, nowMillis int64) {
|
||||
store.GrantWeaponStoryUnlock(user, weaponId, storyIndex, nowMillis)
|
||||
}
|
||||
|
||||
var tutorialCompanionChoices = map[int32]int32{
|
||||
1: 2, // bear + fire (Cat=1, Attr=2)
|
||||
2: 1, // bear + wind (Cat=1, Attr=6)
|
||||
3: 7, // doll + fire (Cat=3, Attr=2)
|
||||
4: 10, // doll + wind (Cat=3, Attr=6)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) ApplyTutorialReward(user *store.UserState, tutorialType model.TutorialType, choiceId int32, nowMillis int64) []RewardGrant {
|
||||
switch tutorialType {
|
||||
case model.TutorialTypeCompanion:
|
||||
return h.applyCompanionTutorialReward(user, choiceId, nowMillis)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyCompanionTutorialReward(user *store.UserState, choiceId int32, nowMillis int64) []RewardGrant {
|
||||
companionId, ok := tutorialCompanionChoices[choiceId]
|
||||
if !ok {
|
||||
log.Printf("[QuestHandler] unknown companion tutorial choiceId=%d", choiceId)
|
||||
return nil
|
||||
}
|
||||
h.grantCompanion(user, companionId, nowMillis)
|
||||
return []RewardGrant{{
|
||||
PossessionType: model.PossessionTypeCompanion,
|
||||
PossessionId: companionId,
|
||||
Count: 1,
|
||||
}}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) BattleDropRewards(questId int32) []masterdata.BattleDropInfo {
|
||||
return h.BattleDropsByQuestId[questId]
|
||||
}
|
||||
|
||||
func (h *QuestHandler) grantWeaponStoryUnlocksForQuestScene(user *store.UserState, questId int32, resultType model.QuestResultType, nowMillis int64) {
|
||||
if resultType == model.QuestResultTypeHalfResult {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||
if reward.PossessionType != model.PossessionTypeWeapon {
|
||||
continue
|
||||
}
|
||||
weaponId := reward.PossessionId
|
||||
weapon, ok := h.WeaponById[weaponId]
|
||||
if !ok || weapon.WeaponStoryReleaseConditionGroupId == 0 {
|
||||
continue
|
||||
}
|
||||
groupId := weapon.WeaponStoryReleaseConditionGroupId
|
||||
for _, cond := range h.ReleaseConditionsByGroupId[groupId] {
|
||||
if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeAcquisition && cond.ConditionValue == 0 {
|
||||
h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if resultType == model.QuestResultTypeFullResult {
|
||||
for groupId, conditions := range h.ReleaseConditionsByGroupId {
|
||||
for _, cond := range conditions {
|
||||
if cond.WeaponStoryReleaseConditionType == model.WeaponStoryReleaseConditionTypeQuestClear && cond.ConditionValue == questId {
|
||||
for _, weaponId := range h.WeaponIdsByReleaseConditionGroupId[groupId] {
|
||||
h.grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package questflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (h *QuestHandler) applySceneGrants(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
grants, ok := h.SceneGrantsBySceneId[questSceneId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, g := range grants {
|
||||
h.applyRewardPossession(user, g.PossessionType, g.PossessionId, g.Count, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) isSceneAhead(newSceneId, currentHeadId int32) bool {
|
||||
if currentHeadId == 0 {
|
||||
return true
|
||||
}
|
||||
return h.SceneById[newSceneId].SortOrder > h.SceneById[currentHeadId].SortOrder
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown sceneId=%d for HandleMainFlowSceneProgress", questSceneId))
|
||||
}
|
||||
|
||||
quest, ok := h.QuestById[scene.QuestId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleMainFlowSceneProgress", questSceneId))
|
||||
}
|
||||
|
||||
user.MainQuest.CurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||
user.MainQuest.HeadQuestSceneId = questSceneId
|
||||
}
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
|
||||
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
lastSceneId := h.getChapterLastSceneId(scene.QuestId)
|
||||
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
||||
|
||||
routeId, ok := h.RouteIdByQuestId[quest.QuestId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleMainFlowSceneProgress setting currentMainQuestRouteId", quest.QuestId))
|
||||
}
|
||||
user.MainQuest.CurrentMainQuestRouteId = routeId
|
||||
|
||||
user.PortalCageStatus.IsCurrentProgress = false
|
||||
user.PortalCageStatus.LatestVersion = nowMillis
|
||||
|
||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) advanceTutorialsForScene(user *store.UserState, sceneId int32) {
|
||||
currentScene, ok := h.SceneById[sceneId]
|
||||
if !ok {
|
||||
log.Printf("[advanceTutorialsForScene] unknown sceneId=%d", sceneId)
|
||||
return
|
||||
}
|
||||
for _, cond := range h.TutorialUnlockConditions {
|
||||
condScene, ok := h.SceneById[cond.ConditionValue]
|
||||
if !ok {
|
||||
log.Printf("[advanceTutorialsForScene] unknown conditionValue=%d", cond.ConditionValue)
|
||||
continue
|
||||
}
|
||||
if currentScene.SortOrder >= condScene.SortOrder {
|
||||
if _, exists := user.Tutorials[cond.TutorialType]; !exists {
|
||||
user.Tutorials[cond.TutorialType] = store.TutorialProgressState{
|
||||
TutorialType: cond.TutorialType,
|
||||
ProgressPhase: 99999,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) getLastMainFlowSceneId(questId int32) int32 {
|
||||
sceneIds := h.SceneIdsByQuestId[questId]
|
||||
if len(sceneIds) == 0 {
|
||||
panic(fmt.Sprintf("no scenes found for questId=%d", questId))
|
||||
}
|
||||
return sceneIds[len(sceneIds)-1]
|
||||
}
|
||||
|
||||
func (h *QuestHandler) getChapterLastSceneId(questId int32) int32 {
|
||||
if id, ok := h.ChapterLastSceneByQuestId[questId]; ok {
|
||||
return id
|
||||
}
|
||||
return h.getLastMainFlowSceneId(questId)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||
if user.MainQuest.ReplayFlowHeadQuestSceneId == 0 || h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||
}
|
||||
user.MainQuest.LatestVersion = nowMillis
|
||||
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d replayHead=%d", questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, questSceneId int32) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown sceneId=%d for HandleMainQuestSceneProgress", questSceneId))
|
||||
}
|
||||
|
||||
quest, ok := h.QuestById[scene.QuestId]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
|
||||
}
|
||||
|
||||
if isMainQuestPlayable(quest) {
|
||||
if scene.QuestResultType == model.QuestResultTypeHalfResult {
|
||||
nowMillis := gametime.NowMillis()
|
||||
h.clearQuestMissions(user, quest.QuestId, nowMillis)
|
||||
}
|
||||
|
||||
user.MainQuest.ProgressQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
||||
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
||||
}
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||
} else {
|
||||
user.MainQuest.CurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||
user.MainQuest.HeadQuestSceneId = questSceneId
|
||||
}
|
||||
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
||||
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type revisionTracker struct {
|
||||
mu sync.RWMutex
|
||||
activeByClient map[string]string
|
||||
lastRevision string
|
||||
}
|
||||
|
||||
type assetResolution struct {
|
||||
ActiveRevision string
|
||||
ListRevision string
|
||||
ListSize int64
|
||||
Candidates []assetCandidate
|
||||
}
|
||||
|
||||
type assetResolver struct{}
|
||||
|
||||
func newRevisionTracker() *revisionTracker {
|
||||
return &revisionTracker{
|
||||
activeByClient: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func newAssetResolver() *assetResolver {
|
||||
return &assetResolver{}
|
||||
}
|
||||
|
||||
func normalizeClientAddr(remoteAddr string) string {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err == nil && host != "" {
|
||||
return host
|
||||
}
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
func (t *revisionTracker) Remember(clientAddr, revision string) {
|
||||
if revision == "" {
|
||||
return
|
||||
}
|
||||
client := normalizeClientAddr(clientAddr)
|
||||
t.mu.Lock()
|
||||
if client != "" {
|
||||
t.activeByClient[client] = revision
|
||||
}
|
||||
t.lastRevision = revision
|
||||
t.mu.Unlock()
|
||||
log.Printf("[Octo] Active list revision for client=%s set to %s", client, revision)
|
||||
}
|
||||
|
||||
func (t *revisionTracker) Active(clientAddr string) string {
|
||||
client := normalizeClientAddr(clientAddr)
|
||||
t.mu.RLock()
|
||||
revision := t.activeByClient[client]
|
||||
if revision == "" {
|
||||
revision = t.lastRevision
|
||||
}
|
||||
t.mu.RUnlock()
|
||||
if revision == "" {
|
||||
return "0"
|
||||
}
|
||||
return revision
|
||||
}
|
||||
|
||||
func (r *assetResolver) Resolve(objectId, assetType, activeRevision string) (assetResolution, bool) {
|
||||
start := time.Now()
|
||||
resolution := assetResolution{ActiveRevision: activeRevision}
|
||||
revision := activeRevision
|
||||
|
||||
candidates, listSize, ok := objectIdToFilePathCandidates(revision, assetType, objectId)
|
||||
if ok && len(candidates) > 0 {
|
||||
resolution.ListRevision = revision
|
||||
resolution.ListSize = listSize
|
||||
resolution.Candidates = candidates
|
||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||
log.Printf("[HTTP] Asset resolve slow: object_id=%s type=%s active_revision=%s list_revision=%s elapsed=%s", objectId, assetType, activeRevision, revision, elapsed)
|
||||
}
|
||||
return resolution, true
|
||||
}
|
||||
|
||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||
log.Printf("[HTTP] Asset resolve miss: object_id=%s type=%s active_revision=%s elapsed=%s", objectId, assetType, activeRevision, elapsed)
|
||||
}
|
||||
return resolution, false
|
||||
}
|
||||
|
||||
func (r *assetResolver) Prewarm(activeRevision string) {
|
||||
if activeRevision == "" {
|
||||
return
|
||||
}
|
||||
_, _ = loadListBinIndex(activeRevision)
|
||||
_ = loadInfoIndex(activeRevision)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type BannerServiceServer struct {
|
||||
pb.UnimplementedBannerServiceServer
|
||||
gacha store.GachaRepository
|
||||
}
|
||||
|
||||
func NewBannerServiceServer(gacha store.GachaRepository) *BannerServiceServer {
|
||||
return &BannerServiceServer{gacha: gacha}
|
||||
}
|
||||
|
||||
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
|
||||
catalog, _ := s.gacha.SnapshotCatalog()
|
||||
var termLimited []*pb.GachaBanner
|
||||
var latestChapter *pb.GachaBanner
|
||||
for _, entry := range catalog {
|
||||
if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle {
|
||||
continue
|
||||
}
|
||||
b := &pb.GachaBanner{
|
||||
GachaLabelType: entry.GachaLabelType,
|
||||
GachaAssetName: entry.BannerAssetName,
|
||||
GachaId: entry.GachaId,
|
||||
}
|
||||
switch entry.GachaLabelType {
|
||||
case model.GachaLabelEvent, model.GachaLabelPremium:
|
||||
termLimited = append(termLimited, b)
|
||||
case model.GachaLabelChapter:
|
||||
if latestChapter == nil || entry.GachaId > latestChapter.GachaId {
|
||||
latestChapter = b
|
||||
}
|
||||
}
|
||||
}
|
||||
return &pb.GetMamaBannerResponse{
|
||||
TermLimitedGacha: termLimited,
|
||||
LatestChapterGacha: latestChapter,
|
||||
IsExistUnreadPop: false,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type BattleServiceServer struct {
|
||||
pb.UnimplementedBattleServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewBattleServiceServer(users store.UserRepository, sessions store.SessionRepository) *BattleServiceServer {
|
||||
return &BattleServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *BattleServiceServer) StartWave(ctx context.Context, req *pb.StartWaveRequest) (*pb.StartWaveResponse, error) {
|
||||
log.Printf("[BattleService] StartWave: userParty=%d npcParty=%d", len(req.UserPartyInitialInfoList), len(req.NpcPartyInitialInfoList))
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Battle.IsActive = true
|
||||
user.Battle.StartCount++
|
||||
user.Battle.LastStartedAt = gametime.NowMillis()
|
||||
user.Battle.LastUserPartyCount = int32(len(req.UserPartyInitialInfoList))
|
||||
user.Battle.LastNpcPartyCount = int32(len(req.NpcPartyInitialInfoList))
|
||||
})
|
||||
return &pb.StartWaveResponse{
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BattleServiceServer) FinishWave(ctx context.Context, req *pb.FinishWaveRequest) (*pb.FinishWaveResponse, error) {
|
||||
log.Printf("[BattleService] FinishWave: battleBinary=%d userParty=%d npcParty=%d elapsedFrames=%d",
|
||||
len(req.BattleBinary), len(req.UserPartyResultInfoList), len(req.NpcPartyResultInfoList), req.ElapsedFrameCount)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Battle.IsActive = false
|
||||
user.Battle.FinishCount++
|
||||
user.Battle.LastFinishedAt = gametime.NowMillis()
|
||||
user.Battle.LastUserPartyCount = int32(len(req.UserPartyResultInfoList))
|
||||
user.Battle.LastNpcPartyCount = int32(len(req.NpcPartyResultInfoList))
|
||||
user.Battle.LastBattleBinarySize = int32(len(req.BattleBinary))
|
||||
user.Battle.LastElapsedFrameCount = req.ElapsedFrameCount
|
||||
})
|
||||
return &pb.FinishWaveResponse{
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type CageOrnamentServiceServer struct {
|
||||
pb.UnimplementedCageOrnamentServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CageOrnamentCatalog
|
||||
granter *store.PossessionGranter
|
||||
}
|
||||
|
||||
func NewCageOrnamentServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CageOrnamentCatalog, granter *store.PossessionGranter) *CageOrnamentServiceServer {
|
||||
return &CageOrnamentServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
||||
}
|
||||
|
||||
func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.ReceiveRewardRequest) (*pb.ReceiveRewardResponse, error) {
|
||||
log.Printf("[CageOrnamentService] ReceiveReward: cageOrnamentId=%d", req.CageOrnamentId)
|
||||
|
||||
reward, ok := s.catalog.LookupReward(req.CageOrnamentId)
|
||||
if !ok {
|
||||
log.Fatalf("[CageOrnamentService] ReceiveReward: no reward for cageOrnamentId=%d", req.CageOrnamentId)
|
||||
}
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.CageOrnamentRewards[req.CageOrnamentId] = store.CageOrnamentRewardState{
|
||||
CageOrnamentId: req.CageOrnamentId,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
||||
})
|
||||
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
|
||||
userdata.FullClientTableMap(user),
|
||||
[]string{
|
||||
"IUserMaterial", "IUserConsumableItem", "IUserGem",
|
||||
"IUserCostume", "IUserCostumeActiveSkill", "IUserCharacter",
|
||||
"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility",
|
||||
"IUserWeaponNote", "IUserWeaponStory",
|
||||
"IUserCageOrnamentReward",
|
||||
},
|
||||
))
|
||||
|
||||
return &pb.ReceiveRewardResponse{
|
||||
CageOrnamentReward: []*pb.CageOrnamentReward{
|
||||
{
|
||||
PossessionType: reward.PossessionType,
|
||||
PossessionId: reward.PossessionId,
|
||||
Count: reward.Count,
|
||||
},
|
||||
},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CageOrnamentServiceServer) RecordAccess(ctx context.Context, req *pb.RecordAccessRequest) (*pb.RecordAccessResponse, error) {
|
||||
log.Printf("[CageOrnamentService] RecordAccess: cageOrnamentId=%d", req.CageOrnamentId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if _, exists := user.CageOrnamentRewards[req.CageOrnamentId]; !exists {
|
||||
user.CageOrnamentRewards[req.CageOrnamentId] = store.CageOrnamentRewardState{
|
||||
CageOrnamentId: req.CageOrnamentId,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
|
||||
userdata.FullClientTableMap(user),
|
||||
[]string{"IUserCageOrnamentReward"},
|
||||
))
|
||||
|
||||
return &pb.RecordAccessResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type CharacterServiceServer struct {
|
||||
pb.UnimplementedCharacterServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CharacterRebirthCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewCharacterServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterRebirthCatalog, config *masterdata.GameConfig) *CharacterServiceServer {
|
||||
return &CharacterServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthRequest) (*pb.RebirthResponse, error) {
|
||||
log.Printf("[CharacterService] Rebirth: characterId=%d rebirthCount=%d", req.CharacterId, req.RebirthCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
stepGroupId, ok := s.catalog.StepGroupByCharacterId[req.CharacterId]
|
||||
if !ok {
|
||||
log.Printf("[CharacterService] Rebirth: no step group for characterId=%d", req.CharacterId)
|
||||
return &pb.RebirthResponse{}, nil
|
||||
}
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
current := user.CharacterRebirths[req.CharacterId]
|
||||
currentCount := current.RebirthCount
|
||||
targetCount := currentCount + req.RebirthCount
|
||||
|
||||
for count := currentCount; count < targetCount; count++ {
|
||||
step, ok := s.catalog.StepByGroupAndCount[masterdata.StepKey{GroupId: stepGroupId, BeforeRebirthCount: count}]
|
||||
if !ok {
|
||||
log.Printf("[CharacterService] Rebirth: no step row for groupId=%d beforeCount=%d", stepGroupId, count)
|
||||
return
|
||||
}
|
||||
|
||||
goldId := s.config.ConsumableItemIdForGold
|
||||
user.ConsumableItems[goldId] = max(user.ConsumableItems[goldId]-s.config.CharacterRebirthConsumeGold, 0)
|
||||
log.Printf("[CharacterService] Rebirth: consumed gold=%d", s.config.CharacterRebirthConsumeGold)
|
||||
|
||||
materials := s.catalog.MaterialsByGroupId[step.CharacterRebirthMaterialGroupId]
|
||||
for _, mat := range materials {
|
||||
user.Materials[mat.MaterialId] -= mat.Count
|
||||
if user.Materials[mat.MaterialId] <= 0 {
|
||||
delete(user.Materials, mat.MaterialId)
|
||||
}
|
||||
log.Printf("[CharacterService] Rebirth: consumed material=%d count=%d", mat.MaterialId, mat.Count)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[CharacterService] Rebirth: characterId=%d count %d -> %d", req.CharacterId, currentCount, targetCount)
|
||||
user.CharacterRebirths[req.CharacterId] = store.CharacterRebirthState{
|
||||
CharacterId: req.CharacterId,
|
||||
RebirthCount: targetCount,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[CharacterService] Rebirth error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rebirthTables := []string{"IUserCharacterRebirth", "IUserMaterial", "IUserConsumableItem"}
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), rebirthTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.RebirthResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type CharacterBoardServiceServer struct {
|
||||
pb.UnimplementedCharacterBoardServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CharacterBoardCatalog
|
||||
}
|
||||
|
||||
func NewCharacterBoardServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterBoardCatalog) *CharacterBoardServiceServer {
|
||||
return &CharacterBoardServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.ReleasePanelRequest) (*pb.ReleasePanelResponse, error) {
|
||||
log.Printf("[CharacterBoardService] ReleasePanel: panelIds=%v", req.CharacterBoardPanelId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}).
|
||||
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, panelId := range req.CharacterBoardPanelId {
|
||||
panel, ok := s.catalog.PanelById[panelId]
|
||||
if !ok {
|
||||
log.Printf("[CharacterBoardService] unknown panelId=%d, skipping", panelId)
|
||||
continue
|
||||
}
|
||||
|
||||
s.consumeCosts(user, panel)
|
||||
s.setReleaseBit(user, panel)
|
||||
s.applyEffects(user, panel)
|
||||
}
|
||||
})
|
||||
|
||||
boardTables := []string{
|
||||
"IUserCharacterBoard",
|
||||
"IUserCharacterBoardAbility",
|
||||
"IUserCharacterBoardStatusUp",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
"IUserGem",
|
||||
}
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(user), boardTables)
|
||||
diff := tracker.Apply(user, tables)
|
||||
|
||||
return &pb.ReleasePanelResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) consumeCosts(user *store.UserState, panel masterdata.CharacterBoardPanelRow) {
|
||||
costs := s.catalog.ReleaseCostsByGroupId[panel.CharacterBoardPanelReleasePossessionGroupId]
|
||||
for _, cost := range costs {
|
||||
store.DeductPossession(user, model.PossessionType(cost.PossessionType), cost.PossessionId, cost.Count)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) setReleaseBit(user *store.UserState, panel masterdata.CharacterBoardPanelRow) {
|
||||
boardId := panel.CharacterBoardId
|
||||
board := user.CharacterBoards[boardId]
|
||||
board.CharacterBoardId = boardId
|
||||
|
||||
bitFieldIndex := (panel.SortOrder - 1) / 32
|
||||
bitPosition := (panel.SortOrder - 1) % 32
|
||||
mask := int32(1 << uint(bitPosition))
|
||||
|
||||
switch bitFieldIndex {
|
||||
case 0:
|
||||
board.PanelReleaseBit1 |= mask
|
||||
case 1:
|
||||
board.PanelReleaseBit2 |= mask
|
||||
case 2:
|
||||
board.PanelReleaseBit3 |= mask
|
||||
case 3:
|
||||
board.PanelReleaseBit4 |= mask
|
||||
}
|
||||
|
||||
user.CharacterBoards[boardId] = board
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) applyEffects(user *store.UserState, panel masterdata.CharacterBoardPanelRow) {
|
||||
effects := s.catalog.ReleaseEffectsByGroupId[panel.CharacterBoardPanelReleaseEffectGroupId]
|
||||
for _, eff := range effects {
|
||||
switch model.CharacterBoardEffectType(eff.CharacterBoardEffectType) {
|
||||
case model.CharacterBoardEffectTypeAbility:
|
||||
s.applyAbilityEffect(user, eff)
|
||||
case model.CharacterBoardEffectTypeStatusUp:
|
||||
s.applyStatusUpEffect(user, eff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) applyAbilityEffect(user *store.UserState, eff masterdata.CharacterBoardReleaseEffectRow) {
|
||||
ability, ok := s.catalog.AbilityById[eff.CharacterBoardEffectId]
|
||||
if !ok {
|
||||
log.Printf("[CharacterBoardService] unknown abilityId=%d", eff.CharacterBoardEffectId)
|
||||
return
|
||||
}
|
||||
|
||||
characterId := s.resolveCharacterId(ability.CharacterBoardEffectTargetGroupId)
|
||||
if characterId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
key := store.CharacterBoardAbilityKey{CharacterId: characterId, AbilityId: ability.AbilityId}
|
||||
state := user.CharacterBoardAbilities[key]
|
||||
state.CharacterId = characterId
|
||||
state.AbilityId = ability.AbilityId
|
||||
state.Level += eff.EffectValue
|
||||
|
||||
if maxLvl, ok := s.catalog.AbilityMaxLevel[key]; ok && state.Level > maxLvl {
|
||||
state.Level = maxLvl
|
||||
}
|
||||
|
||||
user.CharacterBoardAbilities[key] = state
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) applyStatusUpEffect(user *store.UserState, eff masterdata.CharacterBoardReleaseEffectRow) {
|
||||
statusUp, ok := s.catalog.StatusUpById[eff.CharacterBoardEffectId]
|
||||
if !ok {
|
||||
log.Printf("[CharacterBoardService] unknown statusUpId=%d", eff.CharacterBoardEffectId)
|
||||
return
|
||||
}
|
||||
|
||||
characterId := s.resolveCharacterId(statusUp.CharacterBoardEffectTargetGroupId)
|
||||
if characterId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
supType := model.CharacterBoardStatusUpType(statusUp.CharacterBoardStatusUpType)
|
||||
calcType := model.StatusUpTypeToCalcType(supType)
|
||||
|
||||
key := store.CharacterBoardStatusUpKey{
|
||||
CharacterId: characterId,
|
||||
StatusCalculationType: int32(calcType),
|
||||
}
|
||||
state := user.CharacterBoardStatusUps[key]
|
||||
state.CharacterId = characterId
|
||||
state.StatusCalculationType = int32(calcType)
|
||||
|
||||
switch supType {
|
||||
case model.CharacterBoardStatusUpTypeAgilityAdd, model.CharacterBoardStatusUpTypeAgilityMultiply:
|
||||
state.Agility += eff.EffectValue
|
||||
case model.CharacterBoardStatusUpTypeAttackAdd, model.CharacterBoardStatusUpTypeAttackMultiply:
|
||||
state.Attack += eff.EffectValue
|
||||
case model.CharacterBoardStatusUpTypeCritAttackAdd:
|
||||
state.CriticalAttack += eff.EffectValue
|
||||
case model.CharacterBoardStatusUpTypeCritRatioAdd:
|
||||
state.CriticalRatio += eff.EffectValue
|
||||
case model.CharacterBoardStatusUpTypeHpAdd, model.CharacterBoardStatusUpTypeHpMultiply:
|
||||
state.Hp += eff.EffectValue
|
||||
case model.CharacterBoardStatusUpTypeVitalityAdd, model.CharacterBoardStatusUpTypeVitalityMultiply:
|
||||
state.Vitality += eff.EffectValue
|
||||
}
|
||||
|
||||
user.CharacterBoardStatusUps[key] = state
|
||||
}
|
||||
|
||||
func (s *CharacterBoardServiceServer) resolveCharacterId(targetGroupId int32) int32 {
|
||||
targets := s.catalog.EffectTargetsByGroupId[targetGroupId]
|
||||
for _, t := range targets {
|
||||
if t.TargetValue != 0 {
|
||||
return t.TargetValue
|
||||
}
|
||||
}
|
||||
log.Printf("[CharacterBoardService] no characterId resolved for targetGroupId=%d", targetGroupId)
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type CharacterViewerServiceServer struct {
|
||||
pb.UnimplementedCharacterViewerServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CharacterViewerCatalog
|
||||
}
|
||||
|
||||
func NewCharacterViewerServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CharacterViewerCatalog) *CharacterViewerServiceServer {
|
||||
return &CharacterViewerServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _ *emptypb.Empty) (*pb.CharacterViewerTopResponse, error) {
|
||||
log.Printf("[CharacterViewerService] CharacterViewerTop")
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
|
||||
}
|
||||
|
||||
released := s.catalog.ReleasedFieldIds(user)
|
||||
log.Printf("[CharacterViewerService] released %d fields for user %d", len(released), userId)
|
||||
|
||||
now := gametime.NowMillis()
|
||||
records := make([]map[string]any, 0, len(released))
|
||||
for _, fieldId := range released {
|
||||
records = append(records, map[string]any{
|
||||
"userId": userId,
|
||||
"characterViewerFieldId": fieldId,
|
||||
"releaseDatetime": now,
|
||||
"latestVersion": 0,
|
||||
})
|
||||
}
|
||||
|
||||
payload := "[]"
|
||||
if len(records) > 0 {
|
||||
data, _ := json.Marshal(records)
|
||||
payload = string(data)
|
||||
}
|
||||
|
||||
diff := map[string]*pb.DiffData{
|
||||
"IUserCharacterViewerField": {
|
||||
UpdateRecordsJson: payload,
|
||||
DeleteKeysJson: "[]",
|
||||
},
|
||||
}
|
||||
|
||||
return &pb.CharacterViewerTopResponse{
|
||||
ReleaseCharacterViewerFieldId: released,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
const companionMaxLevel = int32(50)
|
||||
|
||||
var companionDiffTables = []string{
|
||||
"IUserCompanion",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
}
|
||||
|
||||
type CompanionServiceServer struct {
|
||||
pb.UnimplementedCompanionServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CompanionCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewCompanionServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CompanionCatalog, config *masterdata.GameConfig) *CompanionServiceServer {
|
||||
return &CompanionServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionEnhanceRequest) (*pb.CompanionEnhanceResponse, error) {
|
||||
log.Printf("[CompanionService] Enhance: uuid=%s addLevel=%d", req.UserCompanionUuid, req.AddLevelCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
companion, ok := user.Companions[req.UserCompanionUuid]
|
||||
if !ok {
|
||||
log.Printf("[CompanionService] Enhance: companion uuid=%s not found", req.UserCompanionUuid)
|
||||
return
|
||||
}
|
||||
|
||||
compDef, ok := s.catalog.CompanionById[companion.CompanionId]
|
||||
if !ok {
|
||||
log.Printf("[CompanionService] Enhance: companion master id=%d not found", companion.CompanionId)
|
||||
return
|
||||
}
|
||||
|
||||
targetLevel := companion.Level + req.AddLevelCount
|
||||
if targetLevel > companionMaxLevel {
|
||||
targetLevel = companionMaxLevel
|
||||
}
|
||||
|
||||
for lvl := companion.Level; lvl < targetLevel; lvl++ {
|
||||
if costFunc, ok := s.catalog.GoldCostByCategory[compDef.CompanionCategoryType]; ok {
|
||||
goldCost := costFunc.Evaluate(lvl)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
}
|
||||
|
||||
matKey := masterdata.CompanionLevelKey{CategoryType: compDef.CompanionCategoryType, Level: lvl}
|
||||
if mat, ok := s.catalog.MaterialsByKey[matKey]; ok {
|
||||
user.Materials[mat.MaterialId] -= mat.Count
|
||||
}
|
||||
}
|
||||
|
||||
companion.Level = targetLevel
|
||||
companion.LatestVersion = nowMillis
|
||||
user.Companions[req.UserCompanionUuid] = companion
|
||||
log.Printf("[CompanionService] Enhance: companionId=%d level -> %d", companion.CompanionId, targetLevel)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("companion enhance: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, companionDiffTables))
|
||||
|
||||
return &pb.CompanionEnhanceResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type ConfigServiceServer struct {
|
||||
pb.UnimplementedConfigServiceServer
|
||||
GrpcHost string
|
||||
GrpcPort int32
|
||||
OctoURL string // HTTP base URL for Octo (list + assets); client uses this instead of default resources.app.nierreincarnation.com
|
||||
}
|
||||
|
||||
func NewConfigServiceServer(host string, port int32, octoURL string) *ConfigServiceServer {
|
||||
return &ConfigServiceServer{GrpcHost: host, GrpcPort: port, OctoURL: octoURL}
|
||||
}
|
||||
|
||||
func (s *ConfigServiceServer) GetReviewServerConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetReviewServerConfigResponse, error) {
|
||||
log.Printf("[ConfigService] GetReviewServerConfig -> %s:%d", s.GrpcHost, s.GrpcPort)
|
||||
|
||||
return &pb.GetReviewServerConfigResponse{
|
||||
Api: &pb.ApiConfig{
|
||||
Hostname: s.GrpcHost,
|
||||
Port: s.GrpcPort,
|
||||
},
|
||||
Octo: &pb.OctoConfig{
|
||||
Version: 1,
|
||||
AppId: 1,
|
||||
ClientSecretKey: "secret",
|
||||
AesKey: "aeskey",
|
||||
Url: s.OctoURL,
|
||||
},
|
||||
WebView: &pb.WebViewConfig{
|
||||
BaseUrl: s.OctoURL,
|
||||
},
|
||||
MasterData: &pb.MasterDataConfig{
|
||||
UrlFormat: s.OctoURL + "/master-data/%s",
|
||||
},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type ContentsStoryServiceServer struct {
|
||||
pb.UnimplementedContentsStoryServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewContentsStoryServiceServer(users store.UserRepository, sessions store.SessionRepository) *ContentsStoryServiceServer {
|
||||
return &ContentsStoryServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *ContentsStoryServiceServer) RegisterPlayed(ctx context.Context, req *pb.ContentsStoryRegisterPlayedRequest) (*pb.ContentsStoryRegisterPlayedResponse, error) {
|
||||
log.Printf("[ContentsStoryService] RegisterPlayed: contentsStoryId=%d", req.ContentsStoryId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.ContentsStories[req.ContentsStoryId] = nowMillis
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserContentsStory"}))
|
||||
|
||||
return &pb.ContentsStoryRegisterPlayedResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
var costumeDiffTables = []string{
|
||||
"IUserCostume",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
}
|
||||
|
||||
type CostumeServiceServer struct {
|
||||
pb.UnimplementedCostumeServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.CostumeCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewCostumeServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.CostumeCatalog, config *masterdata.GameConfig) *CostumeServiceServer {
|
||||
return &CostumeServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceRequest) (*pb.EnhanceResponse, error) {
|
||||
log.Printf("[CostumeService] Enhance: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
costume, ok := user.Costumes[req.UserCostumeUuid]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Enhance: costume uuid=%s not found", req.UserCostumeUuid)
|
||||
return
|
||||
}
|
||||
|
||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Enhance: costume master id=%d not found", costume.CostumeId)
|
||||
return
|
||||
}
|
||||
|
||||
totalExp := int32(0)
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
mat, ok := s.catalog.Materials[materialId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Enhance: material id=%d not found, skipping", materialId)
|
||||
continue
|
||||
}
|
||||
|
||||
cur := user.Materials[materialId]
|
||||
if cur < count {
|
||||
log.Printf("[CostumeService] Enhance: insufficient material id=%d have=%d need=%d", materialId, cur, count)
|
||||
continue
|
||||
}
|
||||
user.Materials[materialId] = cur - count
|
||||
totalMaterialCount += count
|
||||
|
||||
expPerUnit := mat.EffectValue
|
||||
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
||||
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += expPerUnit * count
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[CostumeService] Enhance: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
||||
}
|
||||
|
||||
costume.Exp += totalExp
|
||||
|
||||
if thresholds, ok := s.catalog.ExpByRarity[cm.RarityType]; ok {
|
||||
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
|
||||
}
|
||||
|
||||
costume.LatestVersion = nowMillis
|
||||
user.Costumes[req.UserCostumeUuid] = costume
|
||||
log.Printf("[CostumeService] Enhance: costumeId=%d +%d exp -> total=%d level=%d", costume.CostumeId, totalExp, costume.Exp, costume.Level)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("costume enhance: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
|
||||
|
||||
return &pb.EnhanceResponse{
|
||||
IsGreatSuccess: false,
|
||||
SurplusEnhanceMaterial: map[int32]int32{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var awakenDiffTables = []string{
|
||||
"IUserCostume",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
"IUserCostumeAwakenStatusUp",
|
||||
"IUserThought",
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest) (*pb.AwakenResponse, error) {
|
||||
log.Printf("[CostumeService] Awaken: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
costume, ok := user.Costumes[req.UserCostumeUuid]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Awaken: costume uuid=%s not found", req.UserCostumeUuid)
|
||||
return
|
||||
}
|
||||
|
||||
awakenRow, ok := s.catalog.AwakenByCostumeId[costume.CostumeId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Awaken: no awaken data for costumeId=%d", costume.CostumeId)
|
||||
return
|
||||
}
|
||||
|
||||
nextStep := costume.AwakenCount + 1
|
||||
|
||||
if gold, ok := s.catalog.AwakenPriceByGroup[awakenRow.CostumeAwakenPriceGroupId]; ok {
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= gold
|
||||
log.Printf("[CostumeService] Awaken: gold cost=%d", gold)
|
||||
}
|
||||
|
||||
for materialId, count := range req.Materials {
|
||||
cur := user.Materials[materialId]
|
||||
if cur < count {
|
||||
log.Printf("[CostumeService] Awaken: insufficient material id=%d have=%d need=%d", materialId, cur, count)
|
||||
count = cur
|
||||
}
|
||||
user.Materials[materialId] = cur - count
|
||||
}
|
||||
|
||||
costume.AwakenCount = nextStep
|
||||
costume.LatestVersion = nowMillis
|
||||
user.Costumes[req.UserCostumeUuid] = costume
|
||||
log.Printf("[CostumeService] Awaken: costumeId=%d awakenCount=%d", costume.CostumeId, nextStep)
|
||||
|
||||
effectSteps, ok := s.catalog.AwakenEffectsByGroupAndStep[awakenRow.CostumeAwakenEffectGroupId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
effect, ok := effectSteps[nextStep]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch model.CostumeAwakenEffectType(effect.CostumeAwakenEffectType) {
|
||||
case model.CostumeAwakenEffectTypeStatusUp:
|
||||
s.applyAwakenStatusUp(user, req.UserCostumeUuid, effect.CostumeAwakenEffectId, nowMillis)
|
||||
case model.CostumeAwakenEffectTypeAbility:
|
||||
log.Printf("[CostumeService] Awaken: ability effect id=%d (client-resolved)", effect.CostumeAwakenEffectId)
|
||||
case model.CostumeAwakenEffectTypeItemAcquire:
|
||||
s.applyAwakenItemAcquire(user, effect.CostumeAwakenEffectId, nowMillis)
|
||||
default:
|
||||
log.Printf("[CostumeService] Awaken: unknown effect type=%d", effect.CostumeAwakenEffectType)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("costume awaken: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, awakenDiffTables))
|
||||
|
||||
return &pb.AwakenResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) applyAwakenStatusUp(user *store.UserState, costumeUuid string, statusUpGroupId int32, nowMillis int64) {
|
||||
rows, ok := s.catalog.AwakenStatusUpByGroup[statusUpGroupId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Awaken: status up group %d not found", statusUpGroupId)
|
||||
return
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
calcType := model.StatusCalculationType(row.StatusCalculationType)
|
||||
key := store.CostumeAwakenStatusKey{
|
||||
UserCostumeUuid: costumeUuid,
|
||||
StatusCalculationType: calcType,
|
||||
}
|
||||
state := user.CostumeAwakenStatusUps[key]
|
||||
state.UserCostumeUuid = costumeUuid
|
||||
state.StatusCalculationType = calcType
|
||||
|
||||
switch model.StatusKindType(row.StatusKindType) {
|
||||
case model.StatusKindTypeHp:
|
||||
state.Hp += row.EffectValue
|
||||
case model.StatusKindTypeAttack:
|
||||
state.Attack += row.EffectValue
|
||||
case model.StatusKindTypeVitality:
|
||||
state.Vitality += row.EffectValue
|
||||
case model.StatusKindTypeAgility:
|
||||
state.Agility += row.EffectValue
|
||||
case model.StatusKindTypeCriticalRatio:
|
||||
state.CriticalRatio += row.EffectValue
|
||||
case model.StatusKindTypeCriticalAttack:
|
||||
state.CriticalAttack += row.EffectValue
|
||||
}
|
||||
|
||||
state.LatestVersion = nowMillis
|
||||
user.CostumeAwakenStatusUps[key] = state
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, itemAcquireId int32, nowMillis int64) {
|
||||
acq, ok := s.catalog.AwakenItemAcquireById[itemAcquireId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] Awaken: item acquire id=%d not found", itemAcquireId)
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("awaken-thought-%d", acq.PossessionId)
|
||||
if _, exists := user.Thoughts[key]; exists {
|
||||
return
|
||||
}
|
||||
user.Thoughts[key] = store.ThoughtState{
|
||||
UserThoughtUuid: key,
|
||||
ThoughtId: acq.PossessionId,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
log.Printf("[CostumeService] Awaken: granted thought id=%d", acq.PossessionId)
|
||||
}
|
||||
|
||||
var activeSkillDiffTables = []string{
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.EnhanceActiveSkillRequest) (*pb.EnhanceActiveSkillResponse, error) {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: uuid=%s addLevel=%d", req.UserCostumeUuid, req.AddLevelCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
costume, ok := user.Costumes[req.UserCostumeUuid]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: costume uuid=%s not found", req.UserCostumeUuid)
|
||||
return
|
||||
}
|
||||
|
||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: costume master id=%d not found", costume.CostumeId)
|
||||
return
|
||||
}
|
||||
|
||||
groupRows := s.catalog.ActiveSkillGroupsByGroupId[cm.CostumeActiveSkillGroupId]
|
||||
enhanceMatId := int32(-1)
|
||||
for _, g := range groupRows {
|
||||
if g.CostumeLimitBreakCountLowerLimit <= costume.LimitBreakCount {
|
||||
enhanceMatId = g.CostumeActiveSkillEnhancementMaterialId
|
||||
break
|
||||
}
|
||||
}
|
||||
if enhanceMatId < 0 {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: no skill group for costumeId=%d groupId=%d lb=%d",
|
||||
costume.CostumeId, cm.CostumeActiveSkillGroupId, costume.LimitBreakCount)
|
||||
return
|
||||
}
|
||||
|
||||
skill := user.CostumeActiveSkills[req.UserCostumeUuid]
|
||||
currentLevel := skill.Level
|
||||
|
||||
maxLevelFunc, ok := s.catalog.ActiveSkillMaxLevelByRarity[cm.RarityType]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: no max level func for rarity=%d", cm.RarityType)
|
||||
return
|
||||
}
|
||||
maxLevel := maxLevelFunc.Evaluate(0)
|
||||
|
||||
addCount := req.AddLevelCount
|
||||
if currentLevel+addCount > maxLevel {
|
||||
addCount = maxLevel - currentLevel
|
||||
}
|
||||
if addCount <= 0 {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: already at max level %d", currentLevel)
|
||||
return
|
||||
}
|
||||
|
||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||
key := [2]int32{enhanceMatId, lvl}
|
||||
mats := s.catalog.ActiveSkillEnhanceMats[key]
|
||||
for _, mat := range mats {
|
||||
cur := user.Materials[mat.MaterialId]
|
||||
cost := mat.Count
|
||||
if cur < cost {
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
|
||||
cost = cur
|
||||
}
|
||||
user.Materials[mat.MaterialId] = cur - cost
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.ActiveSkillCostByRarity[cm.RarityType]; ok {
|
||||
goldCost := costFunc.Evaluate(lvl + 1)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
}
|
||||
}
|
||||
|
||||
skill.UserCostumeUuid = req.UserCostumeUuid
|
||||
skill.Level = currentLevel + addCount
|
||||
skill.LatestVersion = nowMillis
|
||||
user.CostumeActiveSkills[req.UserCostumeUuid] = skill
|
||||
log.Printf("[CostumeService] EnhanceActiveSkill: costumeId=%d level %d -> %d", costume.CostumeId, currentLevel, skill.Level)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("costume enhance active skill: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, activeSkillDiffTables))
|
||||
|
||||
return &pb.EnhanceActiveSkillResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBreakRequest) (*pb.LimitBreakResponse, error) {
|
||||
log.Printf("[CostumeService] LimitBreak: uuid=%s materials=%v", req.UserCostumeUuid, req.Materials)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
costume, ok := user.Costumes[req.UserCostumeUuid]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] LimitBreak: costume uuid=%s not found", req.UserCostumeUuid)
|
||||
return
|
||||
}
|
||||
|
||||
if costume.LimitBreakCount >= s.config.CostumeLimitBreakAvailableCount {
|
||||
log.Printf("[CostumeService] LimitBreak: already at max limit break %d", costume.LimitBreakCount)
|
||||
return
|
||||
}
|
||||
|
||||
cm, ok := s.catalog.Costumes[costume.CostumeId]
|
||||
if !ok {
|
||||
log.Printf("[CostumeService] LimitBreak: costume master id=%d not found", costume.CostumeId)
|
||||
return
|
||||
}
|
||||
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
cur := user.Materials[materialId]
|
||||
if cur < count {
|
||||
log.Printf("[CostumeService] LimitBreak: insufficient material id=%d have=%d need=%d", materialId, cur, count)
|
||||
count = cur
|
||||
}
|
||||
user.Materials[materialId] = cur - count
|
||||
totalMaterialCount += count
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.LimitBreakCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[CostumeService] LimitBreak: gold cost=%d", goldCost)
|
||||
}
|
||||
|
||||
costume.LimitBreakCount++
|
||||
costume.LatestVersion = nowMillis
|
||||
user.Costumes[req.UserCostumeUuid] = costume
|
||||
log.Printf("[CostumeService] LimitBreak: costumeId=%d limitBreak -> %d", costume.CostumeId, costume.LimitBreakCount)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("costume limit break: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
|
||||
|
||||
return &pb.LimitBreakResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type DataServiceServer struct {
|
||||
pb.UnimplementedDataServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewDataServiceServer(users store.UserRepository, sessions store.SessionRepository) *DataServiceServer {
|
||||
return &DataServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *DataServiceServer) GetLatestMasterDataVersion(ctx context.Context, _ *emptypb.Empty) (*pb.MasterDataGetLatestVersionResponse, error) {
|
||||
log.Printf("[DataService] GetLatestMasterDataVersion")
|
||||
return &pb.MasterDataGetLatestVersionResponse{
|
||||
LatestMasterDataVersion: "20240404193219",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DataServiceServer) GetUserDataNameV2(ctx context.Context, _ *emptypb.Empty) (*pb.UserDataGetNameResponseV2, error) {
|
||||
log.Printf("[DataService] GetUserDataNameV2")
|
||||
return &pb.UserDataGetNameResponseV2{
|
||||
TableNameList: []*pb.TableNameList{
|
||||
{TableName: defaultTableNames()},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DataServiceServer) GetUserData(ctx context.Context, req *pb.UserDataGetRequest) (*pb.UserDataGetResponse, error) {
|
||||
log.Printf("[DataService] GetUserData: tables=%v", req.TableName)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
defaults := userdata.FirstEntranceClientTableMap(user)
|
||||
result := userdata.SelectTables(defaults, req.TableName)
|
||||
return &pb.UserDataGetResponse{
|
||||
UserDataJson: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func defaultTableNames() []string {
|
||||
return []string{
|
||||
"IUser",
|
||||
"IUserApple",
|
||||
"IUserAutoSaleSettingDetail",
|
||||
"IUserBeginnerCampaign",
|
||||
"IUserBigHuntMaxScore",
|
||||
"IUserBigHuntProgressStatus",
|
||||
"IUserBigHuntScheduleMaxScore",
|
||||
"IUserBigHuntStatus",
|
||||
"IUserBigHuntWeeklyMaxScore",
|
||||
"IUserBigHuntWeeklyStatus",
|
||||
"IUserCageOrnamentReward",
|
||||
"IUserCharacter",
|
||||
"IUserCharacterBoard",
|
||||
"IUserCharacterBoardAbility",
|
||||
"IUserCharacterBoardCompleteReward",
|
||||
"IUserCharacterBoardStatusUp",
|
||||
"IUserCharacterCostumeLevelBonus",
|
||||
"IUserCharacterRebirth",
|
||||
"IUserCharacterViewerField",
|
||||
"IUserComebackCampaign",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserContentsStory",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserCostumeAwakenStatusUp",
|
||||
"IUserCostumeLevelBonusReleaseStatus",
|
||||
"IUserCostumeLotteryEffect",
|
||||
"IUserCostumeLotteryEffectAbility",
|
||||
"IUserCostumeLotteryEffectPending",
|
||||
"IUserCostumeLotteryEffectStatusUp",
|
||||
"IUserDeck",
|
||||
"IUserDeckCharacter",
|
||||
"IUserDeckCharacterDressupCostume",
|
||||
"IUserDeckLimitContentRestricted",
|
||||
"IUserDeckPartsGroup",
|
||||
"IUserDeckSubWeaponGroup",
|
||||
"IUserDeckTypeNote",
|
||||
"IUserDokan",
|
||||
"IUserEventQuestDailyGroupCompleteReward",
|
||||
"IUserEventQuestGuerrillaFreeOpen",
|
||||
"IUserEventQuestLabyrinthSeason",
|
||||
"IUserEventQuestLabyrinthStage",
|
||||
"IUserEventQuestProgressStatus",
|
||||
"IUserEventQuestTowerAccumulationReward",
|
||||
"IUserExplore",
|
||||
"IUserExploreScore",
|
||||
"IUserExtraQuestProgressStatus",
|
||||
"IUserFacebook",
|
||||
"IUserGem",
|
||||
"IUserGimmick",
|
||||
"IUserGimmickOrnamentProgress",
|
||||
"IUserGimmickSequence",
|
||||
"IUserGimmickUnlock",
|
||||
"IUserImportantItem",
|
||||
"IUserLimitedOpen",
|
||||
// "IUserLogin",
|
||||
"IUserLoginBonus",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestReplayFlowStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserMaterial",
|
||||
"IUserMission",
|
||||
"IUserMissionCompletionProgress",
|
||||
"IUserMissionPassPoint",
|
||||
"IUserMovie",
|
||||
"IUserNaviCutIn",
|
||||
"IUserOmikuji",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
"IUserPartsPreset",
|
||||
"IUserPartsPresetTag",
|
||||
"IUserPartsStatusSub",
|
||||
"IUserPortalCageStatus",
|
||||
"IUserPossessionAutoConvert",
|
||||
"IUserPremiumItem",
|
||||
"IUserProfile",
|
||||
"IUserPvpDefenseDeck",
|
||||
"IUserPvpStatus",
|
||||
"IUserPvpWeeklyResult",
|
||||
"IUserQuest",
|
||||
"IUserQuestAutoOrbit",
|
||||
"IUserQuestLimitContentStatus",
|
||||
"IUserQuestMission",
|
||||
"IUserQuestReplayFlowRewardGroup",
|
||||
"IUserQuestSceneChoice",
|
||||
"IUserQuestSceneChoiceHistory",
|
||||
// "IUserSetting",
|
||||
"IUserShopItem",
|
||||
"IUserShopReplaceable",
|
||||
"IUserShopReplaceableLineup",
|
||||
"IUserSideStoryQuest",
|
||||
"IUserSideStoryQuestSceneProgressStatus",
|
||||
"IUserStatus",
|
||||
"IUserThought",
|
||||
"IUserTripleDeck",
|
||||
"IUserTutorialProgress",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponAwaken",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponStory",
|
||||
"IUserWebviewPanelMission",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type DeckServiceServer struct {
|
||||
pb.UnimplementedDeckServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewDeckServiceServer(users store.UserRepository, sessions store.SessionRepository) *DeckServiceServer {
|
||||
return &DeckServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *DeckServiceServer) UpdateName(ctx context.Context, req *pb.UpdateNameRequest) (*pb.UpdateNameResponse, error) {
|
||||
log.Printf("[DeckService] UpdateName: deckType=%d deckNumber=%d name=%q", req.DeckType, req.UserDeckNumber, req.Name)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
deckKey := store.DeckKey{DeckType: model.DeckType(req.DeckType), UserDeckNumber: req.UserDeckNumber}
|
||||
deck := user.Decks[deckKey]
|
||||
deck.Name = req.Name
|
||||
user.Decks[deckKey] = deck
|
||||
})
|
||||
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserDeck"})
|
||||
return &pb.UpdateNameResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeckServiceServer) RefreshDeckPower(ctx context.Context, req *pb.RefreshDeckPowerRequest) (*pb.RefreshDeckPowerResponse, error) {
|
||||
log.Printf("[DeckService] RefreshDeckPower: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if req.DeckPower == nil {
|
||||
log.Printf("[DeckService] RefreshDeckPower: deckPower is nil")
|
||||
return
|
||||
}
|
||||
|
||||
dt := model.DeckType(req.DeckType)
|
||||
deckKey := store.DeckKey{DeckType: dt, UserDeckNumber: req.UserDeckNumber}
|
||||
deck, ok := user.Decks[deckKey]
|
||||
if !ok {
|
||||
log.Fatalf("[DeckService] RefreshDeckPower: deck not found")
|
||||
}
|
||||
|
||||
deck.Power = req.DeckPower.Power
|
||||
user.Decks[deckKey] = deck
|
||||
|
||||
for _, cp := range []*pb.DeckCharacterPower{
|
||||
req.DeckPower.DeckCharacterPower01,
|
||||
req.DeckPower.DeckCharacterPower02,
|
||||
req.DeckPower.DeckCharacterPower03,
|
||||
} {
|
||||
if cp == nil || cp.UserDeckCharacterUuid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if dc, ok := user.DeckCharacters[cp.UserDeckCharacterUuid]; ok {
|
||||
dc.Power = cp.Power
|
||||
user.DeckCharacters[cp.UserDeckCharacterUuid] = dc
|
||||
}
|
||||
}
|
||||
|
||||
note := user.DeckTypeNotes[dt]
|
||||
if req.DeckPower.Power > note.MaxDeckPower {
|
||||
note.DeckType = dt
|
||||
note.MaxDeckPower = req.DeckPower.Power
|
||||
user.DeckTypeNotes[dt] = note
|
||||
}
|
||||
})
|
||||
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
|
||||
})
|
||||
return &pb.RefreshDeckPowerResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeckServiceServer) RefreshMultiDeckPower(ctx context.Context, req *pb.RefreshMultiDeckPowerRequest) (*pb.RefreshMultiDeckPowerResponse, error) {
|
||||
log.Printf("[DeckService] RefreshMultiDeckPower: %d entries", len(req.DeckPowerInfo))
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, info := range req.DeckPowerInfo {
|
||||
if info.DeckPower == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dt := model.DeckType(info.DeckType)
|
||||
deckKey := store.DeckKey{DeckType: dt, UserDeckNumber: info.UserDeckNumber}
|
||||
deck, ok := user.Decks[deckKey]
|
||||
if !ok {
|
||||
log.Printf("[DeckService] RefreshMultiDeckPower: deck not found deckType=%d deckNumber=%d", info.DeckType, info.UserDeckNumber)
|
||||
continue
|
||||
}
|
||||
|
||||
deck.Power = info.DeckPower.Power
|
||||
user.Decks[deckKey] = deck
|
||||
|
||||
for _, cp := range []*pb.DeckCharacterPower{
|
||||
info.DeckPower.DeckCharacterPower01,
|
||||
info.DeckPower.DeckCharacterPower02,
|
||||
info.DeckPower.DeckCharacterPower03,
|
||||
} {
|
||||
if cp == nil || cp.UserDeckCharacterUuid == "" {
|
||||
continue
|
||||
}
|
||||
if dc, ok := user.DeckCharacters[cp.UserDeckCharacterUuid]; ok {
|
||||
dc.Power = cp.Power
|
||||
user.DeckCharacters[cp.UserDeckCharacterUuid] = dc
|
||||
}
|
||||
}
|
||||
|
||||
note := user.DeckTypeNotes[dt]
|
||||
if info.DeckPower.Power > note.MaxDeckPower {
|
||||
note.DeckType = dt
|
||||
note.MaxDeckPower = info.DeckPower.Power
|
||||
user.DeckTypeNotes[dt] = note
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
|
||||
})
|
||||
return &pb.RefreshMultiDeckPowerResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func deckSlotsFromProto(deck *pb.Deck) []store.DeckCharacterInput {
|
||||
slots := make([]store.DeckCharacterInput, 3)
|
||||
for i, ch := range []*pb.DeckCharacter{deck.Character01, deck.Character02, deck.Character03} {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
slots[i] = store.DeckCharacterInput{
|
||||
UserCostumeUuid: ch.UserCostumeUuid,
|
||||
MainUserWeaponUuid: ch.MainUserWeaponUuid,
|
||||
SubWeaponUuids: ch.SubUserWeaponUuid,
|
||||
PartsUuids: ch.UserPartsUuid,
|
||||
UserCompanionUuid: ch.UserCompanionUuid,
|
||||
UserThoughtUuid: ch.UserThoughtUuid,
|
||||
DressupCostumeId: ch.DressupCostumeId,
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeckRequest) (*pb.ReplaceDeckResponse, error) {
|
||||
log.Printf("[DeckService] ReplaceDeck: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber)
|
||||
if req.Deck != nil {
|
||||
for i, ch := range []*pb.DeckCharacter{req.Deck.Character01, req.Deck.Character02, req.Deck.Character03} {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
log.Printf("[DeckService] ReplaceDeck slot %d: costume=%s mainWeapon=%s subWeapons=%v companion=%s thought=%s",
|
||||
i+1, ch.UserCostumeUuid, ch.MainUserWeaponUuid, ch.SubUserWeaponUuid, ch.UserCompanionUuid, ch.UserThoughtUuid)
|
||||
}
|
||||
}
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
|
||||
Track("IUserDeckPartsGroup", oldUser, userdata.DeckPartsGroupRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid", "userPartsUuid"}).
|
||||
Track("IUserDeckCharacterDressupCostume", oldUser, userdata.DeckDressupCostumeRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid"})
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if req.Deck == nil {
|
||||
return
|
||||
}
|
||||
store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis())
|
||||
})
|
||||
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
|
||||
"IUserDeckCharacterDressupCostume",
|
||||
})
|
||||
return &pb.ReplaceDeckResponse{
|
||||
DiffUserData: tracker.Apply(user, result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.ReplaceTripleDeckRequest) (*pb.ReplaceTripleDeckResponse, error) {
|
||||
log.Printf("[DeckService] ReplaceTripleDeck: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
|
||||
Track("IUserDeckPartsGroup", oldUser, userdata.DeckPartsGroupRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid", "userPartsUuid"}).
|
||||
Track("IUserDeckCharacterDressupCostume", oldUser, userdata.DeckDressupCostumeRecords,
|
||||
[]string{"userId", "userDeckCharacterUuid"})
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
for idx, detail := range []*pb.DeckDetail{req.DeckDetail01, req.DeckDetail02, req.DeckDetail03} {
|
||||
if detail == nil || detail.Deck == nil {
|
||||
continue
|
||||
}
|
||||
log.Printf("[DeckService] ReplaceTripleDeck detail %d: deckType=%d deckNumber=%d", idx+1, detail.DeckType, detail.UserDeckNumber)
|
||||
if detail.Deck != nil {
|
||||
for i, ch := range []*pb.DeckCharacter{detail.Deck.Character01, detail.Deck.Character02, detail.Deck.Character03} {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
log.Printf("[DeckService] ReplaceTripleDeck detail %d slot %d: costume=%s mainWeapon=%s subWeapons=%v companion=%s thought=%s",
|
||||
idx+1, i+1, ch.UserCostumeUuid, ch.MainUserWeaponUuid, ch.SubUserWeaponUuid, ch.UserCompanionUuid, ch.UserThoughtUuid)
|
||||
}
|
||||
}
|
||||
store.ApplyDeckReplacement(user, model.DeckType(detail.DeckType), detail.UserDeckNumber, deckSlotsFromProto(detail.Deck), nowMillis)
|
||||
}
|
||||
})
|
||||
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
|
||||
"IUserDeckCharacterDressupCostume",
|
||||
})
|
||||
return &pb.ReplaceTripleDeckResponse{
|
||||
DiffUserData: tracker.Apply(user, result),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type DokanServiceServer struct {
|
||||
pb.UnimplementedDokanServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewDokanServiceServer(users store.UserRepository, sessions store.SessionRepository) *DokanServiceServer {
|
||||
return &DokanServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *DokanServiceServer) RegisterDokanConfirmed(ctx context.Context, req *pb.RegisterDokanConfirmedRequest) (*pb.RegisterDokanConfirmedResponse, error) {
|
||||
log.Printf("[DokanService] RegisterDokanConfirmed: dokanIds=%v", req.DokanId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, id := range req.DokanId {
|
||||
user.DokanConfirmed[id] = true
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserDokan"}))
|
||||
|
||||
return &pb.RegisterDokanConfirmedResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
const (
|
||||
exploreStaminaRecovery = 1000 // millivalue added on finish
|
||||
exploreRewardMaterialId = 100001
|
||||
exploreRewardBaseCount = 1
|
||||
)
|
||||
|
||||
var exploreDiffTables = []string{
|
||||
"IUserExplore",
|
||||
"IUserExploreScore",
|
||||
}
|
||||
|
||||
var exploreFinishDiffTables = []string{
|
||||
"IUserExplore",
|
||||
"IUserExploreScore",
|
||||
"IUserMaterial",
|
||||
"IUserStatus",
|
||||
}
|
||||
|
||||
type ExploreServiceServer struct {
|
||||
pb.UnimplementedExploreServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.ExploreCatalog
|
||||
}
|
||||
|
||||
func NewExploreServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ExploreCatalog) *ExploreServiceServer {
|
||||
return &ExploreServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartExploreRequest) (*pb.StartExploreResponse, error) {
|
||||
log.Printf("[ExploreService] StartExplore: exploreId=%d useConsumableItemId=%d", req.ExploreId, req.UseConsumableItemId)
|
||||
|
||||
if _, ok := s.catalog.Explores[req.ExploreId]; !ok {
|
||||
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
||||
}
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
explore := s.catalog.Explores[req.ExploreId]
|
||||
if req.UseConsumableItemId > 0 && explore.ConsumeItemCount > 0 {
|
||||
cur := user.ConsumableItems[req.UseConsumableItemId]
|
||||
user.ConsumableItems[req.UseConsumableItemId] = cur - explore.ConsumeItemCount
|
||||
log.Printf("[ExploreService] StartExplore: consumed item=%d count=%d remaining=%d", req.UseConsumableItemId, explore.ConsumeItemCount, user.ConsumableItems[req.UseConsumableItemId])
|
||||
}
|
||||
|
||||
user.Explore = store.ExploreState{
|
||||
PlayingExploreId: req.ExploreId,
|
||||
IsUseExploreTicket: false,
|
||||
LatestPlayDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("start explore: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreDiffTables))
|
||||
|
||||
return &pb.StartExploreResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.FinishExploreRequest) (*pb.FinishExploreResponse, error) {
|
||||
log.Printf("[ExploreService] FinishExplore: exploreId=%d score=%d", req.ExploreId, req.Score)
|
||||
|
||||
explore, ok := s.catalog.Explores[req.ExploreId]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("explore id=%d not found", req.ExploreId)
|
||||
}
|
||||
|
||||
assetGradeIconId := s.catalog.GradeForScore(req.ExploreId, req.Score)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
rewardCount := int32(exploreRewardBaseCount) * explore.RewardLotteryCount
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
existing, exists := user.ExploreScores[req.ExploreId]
|
||||
if !exists || req.Score > existing.MaxScore {
|
||||
user.ExploreScores[req.ExploreId] = store.ExploreScoreState{
|
||||
ExploreId: req.ExploreId,
|
||||
MaxScore: req.Score,
|
||||
MaxScoreUpdateDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
user.Explore = store.ExploreState{
|
||||
PlayingExploreId: 0,
|
||||
IsUseExploreTicket: false,
|
||||
LatestPlayDatetime: user.Explore.LatestPlayDatetime,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
|
||||
user.Status.StaminaMilliValue += exploreStaminaRecovery
|
||||
user.Status.StaminaUpdateDatetime = nowMillis
|
||||
user.Status.LatestVersion = nowMillis
|
||||
log.Printf("[ExploreService] FinishExplore: stamina +%d -> %d", exploreStaminaRecovery, user.Status.StaminaMilliValue)
|
||||
|
||||
user.Materials[exploreRewardMaterialId] += rewardCount
|
||||
log.Printf("[ExploreService] FinishExplore: granted material=%d count=%d", exploreRewardMaterialId, rewardCount)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finish explore: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreFinishDiffTables))
|
||||
|
||||
rewards := []*pb.ExploreReward{
|
||||
{
|
||||
PossessionType: int32(model.PossessionTypeMaterial),
|
||||
PossessionId: exploreRewardMaterialId,
|
||||
Count: rewardCount,
|
||||
},
|
||||
}
|
||||
|
||||
return &pb.FinishExploreResponse{
|
||||
AcquireStaminaCount: exploreStaminaRecovery,
|
||||
ExploreReward: rewards,
|
||||
AssetGradeIconId: assetGradeIconId,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExploreServiceServer) RetireExplore(ctx context.Context, req *pb.RetireExploreRequest) (*pb.RetireExploreResponse, error) {
|
||||
log.Printf("[ExploreService] RetireExplore: exploreId=%d", req.ExploreId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Explore = store.ExploreState{
|
||||
PlayingExploreId: 0,
|
||||
IsUseExploreTicket: false,
|
||||
LatestPlayDatetime: user.Explore.LatestPlayDatetime,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retire explore: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserExplore"}))
|
||||
|
||||
return &pb.RetireExploreResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type FriendServiceServer struct {
|
||||
pb.UnimplementedFriendServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewFriendServiceServer(users store.UserRepository, sessions store.SessionRepository) *FriendServiceServer {
|
||||
return &FriendServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *FriendServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
|
||||
log.Printf("[FriendService] GetUser: playerId=%d", req.PlayerId)
|
||||
return &pb.GetUserResponse{DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *FriendServiceServer) GetFriendList(ctx context.Context, req *pb.GetFriendListRequest) (*pb.GetFriendListResponse, error) {
|
||||
log.Printf("[FriendService] GetFriendList")
|
||||
return &pb.GetFriendListResponse{
|
||||
FriendUser: []*pb.FriendUser{},
|
||||
SendCheerCount: 0,
|
||||
ReceivedCheerCount: 0,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FriendServiceServer) GetFriendRequestList(ctx context.Context, req *emptypb.Empty) (*pb.GetFriendRequestListResponse, error) {
|
||||
log.Printf("[FriendService] GetFriendRequestList")
|
||||
return &pb.GetFriendRequestListResponse{
|
||||
User: []*pb.User{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FriendServiceServer) SearchRecommendedUsers(ctx context.Context, req *emptypb.Empty) (*pb.SearchRecommendedUsersResponse, error) {
|
||||
log.Printf("[FriendService] SearchRecommendedUsers")
|
||||
return &pb.SearchRecommendedUsersResponse{
|
||||
Users: []*pb.User{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gacha"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
var gachaDiffTables = []string{
|
||||
"IUserGem",
|
||||
"IUserCostume",
|
||||
"IUserWeapon",
|
||||
"IUserConsumableItem",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponStory",
|
||||
"IUserCharacter",
|
||||
"IUserMaterial",
|
||||
}
|
||||
|
||||
type GachaServiceServer struct {
|
||||
pb.UnimplementedGachaServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
gacha store.GachaRepository
|
||||
handler *gacha.GachaHandler
|
||||
}
|
||||
|
||||
func NewGachaServiceServer(
|
||||
users store.UserRepository,
|
||||
sessions store.SessionRepository,
|
||||
gachaRepo store.GachaRepository,
|
||||
handler *gacha.GachaHandler,
|
||||
) *GachaServiceServer {
|
||||
return &GachaServiceServer{
|
||||
users: users,
|
||||
sessions: sessions,
|
||||
gacha: gachaRepo,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
|
||||
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
|
||||
|
||||
catalog, _ := s.gacha.SnapshotCatalog()
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
user, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.EnsureMaps()
|
||||
s.autoConvertExpiredMedals(user, catalog, nowMillis)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
gachaList := make([]*pb.Gacha, 0, len(catalog))
|
||||
for _, entry := range catalog {
|
||||
if !matchesGachaLabel(req.GachaLabelType, entry.GachaLabelType) {
|
||||
continue
|
||||
}
|
||||
if entry.GachaLabelType == model.GachaLabelPortalCage || entry.GachaLabelType == model.GachaLabelRecycle {
|
||||
continue
|
||||
}
|
||||
bs := user.Gacha.BannerStates[entry.GachaId]
|
||||
gachaList = append(gachaList, toProtoGacha(entry, &bs))
|
||||
}
|
||||
|
||||
return &pb.GetGachaListResponse{
|
||||
Gacha: gachaList,
|
||||
ConvertedGachaMedal: toProtoConvertedGachaMedal(user.Gacha.ConvertedGachaMedal),
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, catalog []store.GachaCatalogEntry, nowMillis int64) {
|
||||
for _, entry := range catalog {
|
||||
if entry.GachaMedalId == 0 || entry.EndDatetime == 0 {
|
||||
continue
|
||||
}
|
||||
if nowMillis < entry.EndDatetime {
|
||||
continue
|
||||
}
|
||||
bs, exists := user.Gacha.BannerStates[entry.GachaId]
|
||||
if !exists || bs.MedalCount <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
medalInfo, ok := s.handler.MedalInfo[entry.GachaId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
conversionRate := medalInfo.ConversionRate
|
||||
if conversionRate <= 0 {
|
||||
conversionRate = 1
|
||||
}
|
||||
bookmarkCount := bs.MedalCount * conversionRate
|
||||
|
||||
user.ConsumableItems[medalInfo.ConsumableItemId] += bookmarkCount
|
||||
|
||||
user.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = append(
|
||||
user.Gacha.ConvertedGachaMedal.ConvertedMedalPossession,
|
||||
store.ConsumableItemState{
|
||||
ConsumableItemId: medalInfo.ConsumableItemId,
|
||||
Count: bookmarkCount,
|
||||
},
|
||||
)
|
||||
|
||||
originalCount := bs.MedalCount
|
||||
bs.MedalCount = 0
|
||||
user.Gacha.BannerStates[entry.GachaId] = bs
|
||||
|
||||
log.Printf("[GachaService] auto-converted %d medals for gacha %d -> %d bookmarks (item %d)",
|
||||
originalCount, entry.GachaId, bookmarkCount, medalInfo.ConsumableItemId)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
|
||||
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
|
||||
|
||||
catalog, _ := s.gacha.SnapshotCatalog()
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
byId := make(map[int32]*pb.Gacha, len(req.GachaId))
|
||||
for _, wantedId := range req.GachaId {
|
||||
for _, entry := range catalog {
|
||||
if entry.GachaId == wantedId {
|
||||
bs := user.Gacha.BannerStates[entry.GachaId]
|
||||
byId[wantedId] = toProtoGacha(entry, &bs)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.GetGachaResponse{
|
||||
Gacha: byId,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
|
||||
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
|
||||
|
||||
catalog, _ := s.gacha.SnapshotCatalog()
|
||||
entry := findCatalogEntry(catalog, req.GachaId)
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
||||
}
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
execCount := req.ExecCount
|
||||
if execCount <= 0 {
|
||||
execCount = 1
|
||||
}
|
||||
|
||||
var drawResult *gacha.DrawResult
|
||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
var drawErr error
|
||||
drawResult, drawErr = s.handler.HandleDraw(user, *entry, req.GachaPricePhaseId, execCount)
|
||||
if drawErr != nil {
|
||||
log.Printf("[GachaService] Draw error: %v", drawErr)
|
||||
drawResult = &gacha.DrawResult{}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
for i, item := range drawResult.Items {
|
||||
if bonus, ok := drawResult.BonusItems[i]; ok {
|
||||
log.Printf("[GachaService] drawn[%d]: type=%d id=%d rarity=%d + bonus type=%d id=%d rarity=%d",
|
||||
i, item.PossessionType, item.PossessionId, item.RarityType,
|
||||
bonus.PossessionType, bonus.PossessionId, bonus.RarityType)
|
||||
} else {
|
||||
log.Printf("[GachaService] drawn[%d]: type=%d id=%d rarity=%d",
|
||||
i, item.PossessionType, item.PossessionId, item.RarityType)
|
||||
}
|
||||
}
|
||||
|
||||
gachaResults := make([]*pb.DrawGachaOddsItem, 0, len(drawResult.Items))
|
||||
dupMap := make(map[int]gacha.DuplicateInfo)
|
||||
for _, d := range drawResult.DuplicateInfos {
|
||||
dupMap[d.Index] = d
|
||||
}
|
||||
bonusDupMap := make(map[int]gacha.DuplicateInfo)
|
||||
for _, d := range drawResult.BonusDuplicateInfos {
|
||||
bonusDupMap[d.Index] = d
|
||||
}
|
||||
|
||||
costumePT := int32(model.PossessionTypeCostume)
|
||||
weaponPT := int32(model.PossessionTypeWeapon)
|
||||
isMaterialDraw := model.IsMaterialBanner(entry.GachaLabelType)
|
||||
|
||||
ownedCostumes := make(map[int32]bool, len(updatedUser.Costumes))
|
||||
for _, c := range updatedUser.Costumes {
|
||||
ownedCostumes[c.CostumeId] = true
|
||||
}
|
||||
ownedWeapons := make(map[int32]bool, len(updatedUser.Weapons))
|
||||
for _, w := range updatedUser.Weapons {
|
||||
ownedWeapons[w.WeaponId] = true
|
||||
}
|
||||
|
||||
for i, item := range drawResult.Items {
|
||||
isNew := !isOwnedByType(item, ownedCostumes, ownedWeapons, updatedUser)
|
||||
|
||||
var oddsItem *pb.DrawGachaOddsItem
|
||||
|
||||
if isMaterialDraw {
|
||||
oddsItem = &pb.DrawGachaOddsItem{
|
||||
GachaItem: &pb.GachaItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: 1,
|
||||
IsNew: isNew,
|
||||
},
|
||||
GachaItemBonus: &pb.GachaItem{},
|
||||
}
|
||||
} else if bonus, hasBonusWeapon := drawResult.BonusItems[i]; hasBonusWeapon {
|
||||
oddsItem = &pb.DrawGachaOddsItem{
|
||||
GachaItem: &pb.GachaItem{
|
||||
PossessionType: costumePT,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: 1,
|
||||
IsNew: isNew,
|
||||
},
|
||||
GachaItemBonus: &pb.GachaItem{
|
||||
PossessionType: weaponPT,
|
||||
PossessionId: bonus.PossessionId,
|
||||
Count: 1,
|
||||
IsNew: !ownedWeapons[bonus.PossessionId],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
oddsItem = &pb.DrawGachaOddsItem{
|
||||
GachaItem: &pb.GachaItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: 1,
|
||||
IsNew: isNew,
|
||||
},
|
||||
GachaItemBonus: &pb.GachaItem{},
|
||||
}
|
||||
}
|
||||
|
||||
if drawResult.MedalBonus > 0 && entry.MedalConsumableItemId != 0 {
|
||||
oddsItem.MedalBonus = &pb.GachaBonus{
|
||||
PossessionType: int32(model.PossessionTypeConsumableItem),
|
||||
PossessionId: entry.MedalConsumableItemId,
|
||||
Count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if dup, ok := dupMap[i]; ok {
|
||||
applyDuplicationBonus(oddsItem, dup)
|
||||
}
|
||||
if bdup, ok := bonusDupMap[i]; ok {
|
||||
applyDuplicationBonus(oddsItem, bdup)
|
||||
}
|
||||
|
||||
gachaResults = append(gachaResults, oddsItem)
|
||||
}
|
||||
|
||||
var bonuses []*pb.GachaBonus
|
||||
for _, b := range drawResult.Bonuses {
|
||||
bonuses = append(bonuses, &pb.GachaBonus{
|
||||
PossessionType: b.PossessionType,
|
||||
PossessionId: b.PossessionId,
|
||||
Count: b.Count,
|
||||
})
|
||||
}
|
||||
|
||||
bs := updatedUser.Gacha.BannerStates[entry.GachaId]
|
||||
nextGacha := toProtoGacha(*entry, &bs)
|
||||
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
|
||||
userdata.FullClientTableMap(updatedUser),
|
||||
gachaDiffTables,
|
||||
))
|
||||
|
||||
return &pb.DrawResponse{
|
||||
NextGacha: nextGacha,
|
||||
GachaResult: gachaResults,
|
||||
GachaBonus: bonuses,
|
||||
MenuGachaBadgeInfo: []*pb.MenuGachaBadgeInfo{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
|
||||
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
|
||||
|
||||
catalog, _ := s.gacha.SnapshotCatalog()
|
||||
entry := findCatalogEntry(catalog, req.GachaId)
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
|
||||
}
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if resetErr := s.handler.HandleResetBox(user, *entry); resetErr != nil {
|
||||
log.Printf("[GachaService] ResetBoxGacha error: %v", resetErr)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
bs := updatedUser.Gacha.BannerStates[entry.GachaId]
|
||||
|
||||
return &pb.ResetBoxGachaResponse{
|
||||
Gacha: toProtoGacha(*entry, &bs),
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Empty) (*pb.GetRewardGachaResponse, error) {
|
||||
log.Printf("[GachaService] GetRewardGacha")
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
maxCount := s.handler.Config.RewardGachaDailyMaxCount
|
||||
if maxCount <= 0 {
|
||||
maxCount = model.DefaultDailyDrawLimit
|
||||
}
|
||||
|
||||
todayStart := gametime.StartOfDayMillis()
|
||||
drawCount := user.Gacha.TodaysCurrentDrawCount
|
||||
if user.Gacha.LastRewardDrawDate < todayStart {
|
||||
drawCount = 0
|
||||
}
|
||||
|
||||
return &pb.GetRewardGachaResponse{
|
||||
Available: drawCount < maxCount,
|
||||
TodaysCurrentDrawCount: drawCount,
|
||||
DailyMaxCount: maxCount,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GachaServiceServer) RewardDraw(ctx context.Context, req *pb.RewardDrawRequest) (*pb.RewardDrawResponse, error) {
|
||||
log.Printf("[GachaService] RewardDraw: placement=%q reward=%q amount=%q", req.PlacementName, req.RewardName, req.RewardAmount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
var items []gacha.DrawnItem
|
||||
updatedUser, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
var drawErr error
|
||||
items, drawErr = s.handler.HandleRewardDraw(user, 1)
|
||||
if drawErr != nil {
|
||||
log.Printf("[GachaService] RewardDraw error: %v", drawErr)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
ownedCostumes := make(map[int32]bool, len(updatedUser.Costumes))
|
||||
for _, c := range updatedUser.Costumes {
|
||||
ownedCostumes[c.CostumeId] = true
|
||||
}
|
||||
ownedWeapons := make(map[int32]bool, len(updatedUser.Weapons))
|
||||
for _, w := range updatedUser.Weapons {
|
||||
ownedWeapons[w.WeaponId] = true
|
||||
}
|
||||
|
||||
results := make([]*pb.RewardGachaItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, &pb.RewardGachaItem{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: 1,
|
||||
IsNew: !isOwnedByType(item, ownedCostumes, ownedWeapons, updatedUser),
|
||||
})
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(updatedUser)
|
||||
diff := userdata.BuildDiffFromTables(tables)
|
||||
|
||||
return &pb.RewardDrawResponse{
|
||||
RewardGachaResult: results,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func findCatalogEntry(catalog []store.GachaCatalogEntry, gachaId int32) *store.GachaCatalogEntry {
|
||||
for i := range catalog {
|
||||
if catalog[i].GachaId == gachaId {
|
||||
return &catalog[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchesGachaLabel(labels []int32, label int32) bool {
|
||||
if len(labels) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, candidate := range labels {
|
||||
if candidate == label {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func toProtoGacha(entry store.GachaCatalogEntry, bs *store.GachaBannerState) *pb.Gacha {
|
||||
g := &pb.Gacha{
|
||||
GachaId: entry.GachaId,
|
||||
GachaLabelType: entry.GachaLabelType,
|
||||
GachaModeType: entry.GachaModeType,
|
||||
GachaAutoResetType: entry.GachaAutoResetType,
|
||||
GachaAutoResetPeriod: entry.GachaAutoResetPeriod,
|
||||
NextAutoResetDatetime: safeTimestamp(entry.NextAutoResetDatetime),
|
||||
GachaUnlockCondition: []*pb.GachaUnlockCondition{{GachaUnlockConditionType: model.GachaUnlockNone, ConditionValue: 0}},
|
||||
IsUserGachaUnlock: entry.IsUserGachaUnlock,
|
||||
StartDatetime: safeTimestamp(entry.StartDatetime),
|
||||
EndDatetime: safeTimestamp(entry.EndDatetime),
|
||||
RelatedMainQuestChapterId: entry.RelatedMainQuestChapterId,
|
||||
RelatedEventQuestChapterId: entry.RelatedEventQuestChapterId,
|
||||
PromotionMovieAssetId: entry.PromotionMovieAssetId,
|
||||
GachaMedalId: entry.GachaMedalId,
|
||||
GachaDecorationType: entry.GachaDecorationType,
|
||||
SortOrder: entry.SortOrder,
|
||||
IsInactive: entry.IsInactive,
|
||||
InformationId: entry.InformationId,
|
||||
}
|
||||
|
||||
g.GachaPricePhase = buildProtoPricePhases(entry, bs)
|
||||
|
||||
promotionItems := buildProtoPromotionItems(entry)
|
||||
|
||||
switch entry.GachaModeType {
|
||||
case model.GachaModeBox:
|
||||
boxNumber := int32(1)
|
||||
if bs != nil && bs.BoxNumber > 0 {
|
||||
boxNumber = bs.BoxNumber
|
||||
}
|
||||
phaseId := int32(0)
|
||||
if len(entry.PricePhases) > 0 {
|
||||
phaseId = entry.PricePhases[0].PhaseId
|
||||
}
|
||||
g.GachaMode = &pb.Gacha_GachaModeBoxComposition{
|
||||
GachaModeBoxComposition: &pb.GachaModeBoxComposition{
|
||||
GachaBoxGroupId: entry.GroupId,
|
||||
BoxNumber: boxNumber,
|
||||
CurrentBoxNumber: boxNumber,
|
||||
NaviCharacterCommentAssetName: "production",
|
||||
GachaAssetName: entry.BannerAssetName,
|
||||
GachaPricePhaseId: phaseId,
|
||||
PromotionGachaOddsItem: promotionItems,
|
||||
GachaDescriptionTextId: entry.DescriptionTextId,
|
||||
},
|
||||
}
|
||||
case model.GachaModeStepup:
|
||||
stepNumber := int32(1)
|
||||
loopCount := int32(0)
|
||||
if bs != nil {
|
||||
if bs.StepNumber > 0 {
|
||||
stepNumber = bs.StepNumber
|
||||
}
|
||||
loopCount = bs.LoopCount
|
||||
}
|
||||
g.GachaMode = &pb.Gacha_GachaModeStepupComposition{
|
||||
GachaModeStepupComposition: &pb.GachaModeStepupComposition{
|
||||
GachaStepGroupId: entry.GroupId,
|
||||
StepNumber: 1,
|
||||
CurrentStepNumber: stepNumber,
|
||||
NaviCharacterCommentAssetName: "production",
|
||||
GachaAssetName: entry.BannerAssetName,
|
||||
PromotionGachaOddsItem: promotionItems,
|
||||
CurrentLoopCount: loopCount,
|
||||
},
|
||||
}
|
||||
default:
|
||||
g.GachaMode = &pb.Gacha_GachaModeBasic{
|
||||
GachaModeBasic: &pb.GachaModeBasic{
|
||||
NaviCharacterCommentAssetName: "production",
|
||||
GachaAssetName: entry.BannerAssetName,
|
||||
PromotionGachaOddsItem: promotionItems,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func buildProtoPricePhases(entry store.GachaCatalogEntry, bs *store.GachaBannerState) []*pb.GachaPricePhase {
|
||||
phases := make([]*pb.GachaPricePhase, 0, len(entry.PricePhases))
|
||||
|
||||
for _, p := range entry.PricePhases {
|
||||
isEnabled := true
|
||||
if entry.GachaModeType == model.GachaModeStepup && bs != nil {
|
||||
currentStep := bs.StepNumber
|
||||
if currentStep <= 0 {
|
||||
currentStep = 1
|
||||
}
|
||||
isEnabled = p.StepNumber == currentStep
|
||||
}
|
||||
|
||||
var bonuses []*pb.GachaBonus
|
||||
for _, b := range p.Bonuses {
|
||||
bonuses = append(bonuses, &pb.GachaBonus{
|
||||
PossessionType: b.PossessionType,
|
||||
PossessionId: b.PossessionId,
|
||||
Count: b.Count,
|
||||
})
|
||||
}
|
||||
|
||||
limitExec := p.LimitExecCount
|
||||
if limitExec <= 0 {
|
||||
limitExec = 999
|
||||
}
|
||||
|
||||
phases = append(phases, &pb.GachaPricePhase{
|
||||
GachaPricePhaseId: p.PhaseId,
|
||||
IsEnabled: isEnabled,
|
||||
EndDatetime: safeTimestamp(entry.EndDatetime),
|
||||
PriceType: p.PriceType,
|
||||
PriceId: p.PriceId,
|
||||
Price: p.Price,
|
||||
RegularPrice: p.RegularPrice,
|
||||
DrawCount: p.DrawCount,
|
||||
LimitExecCount: limitExec,
|
||||
EachMaxExecCount: p.DrawCount,
|
||||
GachaBonus: bonuses,
|
||||
GachaOddsFixedRarity: &pb.GachaOddsFixedRarity{
|
||||
FixedRarityTypeLowerLimit: p.FixedRarityMin,
|
||||
FixedCount: p.FixedCount,
|
||||
},
|
||||
GachaBadgeType: model.GachaBadgeTypeNone,
|
||||
})
|
||||
}
|
||||
|
||||
return phases
|
||||
}
|
||||
|
||||
func buildProtoPromotionItems(entry store.GachaCatalogEntry) []*pb.GachaOddsItem {
|
||||
if len(entry.PromotionItems) == 0 {
|
||||
return nil
|
||||
}
|
||||
isMaterial := model.IsMaterialBanner(entry.GachaLabelType)
|
||||
|
||||
items := make([]*pb.GachaOddsItem, 0, len(entry.PromotionItems))
|
||||
for i, pi := range entry.PromotionItems {
|
||||
bonus := &pb.GachaItem{}
|
||||
if !isMaterial && pi.BonusPossessionType != 0 {
|
||||
bonus = &pb.GachaItem{
|
||||
PossessionType: pi.BonusPossessionType,
|
||||
PossessionId: pi.BonusPossessionId,
|
||||
Count: 1,
|
||||
}
|
||||
}
|
||||
items = append(items, &pb.GachaOddsItem{
|
||||
GachaItem: &pb.GachaItem{
|
||||
PossessionType: pi.PossessionType,
|
||||
PossessionId: pi.PossessionId,
|
||||
Count: 1,
|
||||
PromotionOrder: int32(i + 1),
|
||||
},
|
||||
GachaItemBonus: bonus,
|
||||
MaxDrawableCount: 999,
|
||||
IsTarget: pi.IsTarget,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func toProtoConvertedGachaMedal(state store.ConvertedGachaMedalState) *pb.ConvertedGachaMedal {
|
||||
items := make([]*pb.ConsumableItemPossession, 0, len(state.ConvertedMedalPossession))
|
||||
for _, item := range state.ConvertedMedalPossession {
|
||||
items = append(items, &pb.ConsumableItemPossession{
|
||||
ConsumableItemId: item.ConsumableItemId,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
|
||||
obtain := &pb.ConsumableItemPossession{
|
||||
ConsumableItemId: 0,
|
||||
Count: 0,
|
||||
}
|
||||
if state.ObtainPossession != nil {
|
||||
obtain.ConsumableItemId = state.ObtainPossession.ConsumableItemId
|
||||
obtain.Count = state.ObtainPossession.Count
|
||||
}
|
||||
|
||||
return &pb.ConvertedGachaMedal{
|
||||
ConvertedMedalPossession: items,
|
||||
ObtainPossession: obtain,
|
||||
}
|
||||
}
|
||||
|
||||
func safeTimestamp(unixMillis int64) *timestamppb.Timestamp {
|
||||
if unixMillis == 0 {
|
||||
return ×tamppb.Timestamp{Seconds: 0}
|
||||
}
|
||||
return timestamppb.New(time.UnixMilli(unixMillis))
|
||||
}
|
||||
|
||||
func applyDuplicationBonus(oddsItem *pb.DrawGachaOddsItem, dup gacha.DuplicateInfo) {
|
||||
if oddsItem.DuplicationBonusGrade == 0 {
|
||||
oddsItem.DuplicationBonusGrade = dup.Grade
|
||||
}
|
||||
for _, b := range dup.Bonuses {
|
||||
oddsItem.DuplicationBonus = append(oddsItem.DuplicationBonus, &pb.GachaBonus{
|
||||
PossessionType: b.PossessionType,
|
||||
PossessionId: b.PossessionId,
|
||||
Count: b.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isOwnedByType(item gacha.DrawnItem, costumes, weapons map[int32]bool, user store.UserState) bool {
|
||||
switch item.PossessionType {
|
||||
case int32(model.PossessionTypeCostume):
|
||||
return costumes[item.PossessionId]
|
||||
case int32(model.PossessionTypeWeapon):
|
||||
return weapons[item.PossessionId]
|
||||
case int32(model.PossessionTypeMaterial):
|
||||
return user.Materials[item.PossessionId] > 0
|
||||
case int32(model.PossessionTypeWeaponEnhanced):
|
||||
return user.ConsumableItems[item.PossessionId] > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type GameplayServiceServer struct {
|
||||
pb.UnimplementedGamePlayServiceServer
|
||||
}
|
||||
|
||||
func NewGameplayServiceServer() *GameplayServiceServer {
|
||||
return &GameplayServiceServer{}
|
||||
}
|
||||
|
||||
func (s *GameplayServiceServer) CheckBeforeGamePlay(ctx context.Context, req *pb.CheckBeforeGamePlayRequest) (*pb.CheckBeforeGamePlayResponse, error) {
|
||||
log.Printf("[GamePlayService] CheckBeforeGamePlay: tr=%s voiceLang=%d textLang=%d",
|
||||
req.Tr, req.VoiceClientSystemLanguageTypeId, req.TextClientSystemLanguageTypeId)
|
||||
|
||||
return &pb.CheckBeforeGamePlayResponse{
|
||||
IsExistUnreadPop: false,
|
||||
MenuGachaBadgeInfo: []*pb.MenuGachaBadgeInfo{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type GiftServiceServer struct {
|
||||
pb.UnimplementedGiftServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewGiftServiceServer(users store.UserRepository, sessions store.SessionRepository) *GiftServiceServer {
|
||||
return &GiftServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *GiftServiceServer) ReceiveGift(ctx context.Context, req *pb.ReceiveGiftRequest) (*pb.ReceiveGiftResponse, error) {
|
||||
log.Printf("[GiftService] ReceiveGift: giftUuids=%d", len(req.UserGiftUuid))
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
received := make([]string, 0, len(req.UserGiftUuid))
|
||||
_, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
remaining := make([]store.NotReceivedGiftState, 0, len(user.Gifts.NotReceived))
|
||||
for _, gift := range user.Gifts.NotReceived {
|
||||
if slices.Contains(req.UserGiftUuid, gift.UserGiftUuid) {
|
||||
received = append(received, gift.UserGiftUuid)
|
||||
user.Gifts.Received = append(user.Gifts.Received, store.ReceivedGiftState{
|
||||
GiftCommon: gift.GiftCommon,
|
||||
ReceivedDatetime: nowMillis,
|
||||
})
|
||||
continue
|
||||
}
|
||||
remaining = append(remaining, gift)
|
||||
}
|
||||
user.Gifts.NotReceived = remaining
|
||||
user.Notifications.GiftNotReceiveCount = int32(len(user.Gifts.NotReceived))
|
||||
})
|
||||
if err != nil {
|
||||
return &pb.ReceiveGiftResponse{
|
||||
ReceivedGiftUuid: []string{},
|
||||
ExpiredGiftUuid: []string{},
|
||||
OverflowGiftUuid: []string{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &pb.ReceiveGiftResponse{
|
||||
ReceivedGiftUuid: received,
|
||||
ExpiredGiftUuid: []string{},
|
||||
OverflowGiftUuid: []string{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftListRequest) (*pb.GetGiftListResponse, error) {
|
||||
log.Printf("[GiftService] GetGiftList: rewardKinds=%v expirationType=%d ascending=%v nextCursor=%d previousCursor=%d getCount=%d",
|
||||
req.RewardKindType, req.ExpirationType, req.IsAscendingSort, req.NextCursor, req.PreviousCursor, req.GetCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
gifts := append([]store.NotReceivedGiftState(nil), user.Gifts.NotReceived...)
|
||||
sort.Slice(gifts, func(i, j int) bool {
|
||||
if req.IsAscendingSort {
|
||||
return gifts[i].ExpirationDatetime < gifts[j].ExpirationDatetime
|
||||
}
|
||||
return gifts[i].ExpirationDatetime > gifts[j].ExpirationDatetime
|
||||
})
|
||||
if req.GetCount > 0 && len(gifts) > int(req.GetCount) {
|
||||
gifts = gifts[:req.GetCount]
|
||||
}
|
||||
|
||||
items := make([]*pb.NotReceivedGift, 0, len(gifts))
|
||||
for _, gift := range gifts {
|
||||
items = append(items, &pb.NotReceivedGift{
|
||||
GiftCommon: toProtoGiftCommon(gift.GiftCommon),
|
||||
ExpirationDatetime: timestampOrNilGift(gift.ExpirationDatetime),
|
||||
UserGiftUuid: gift.UserGiftUuid,
|
||||
})
|
||||
}
|
||||
|
||||
return &pb.GetGiftListResponse{
|
||||
Gift: items,
|
||||
TotalPageCount: pageCount(len(user.Gifts.NotReceived), int(req.GetCount)),
|
||||
NextCursor: 0,
|
||||
PreviousCursor: 0,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GiftServiceServer) GetGiftReceiveHistoryList(ctx context.Context, req *emptypb.Empty) (*pb.GetGiftReceiveHistoryListResponse, error) {
|
||||
log.Printf("[GiftService] GetGiftReceiveHistoryList")
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
items := make([]*pb.ReceivedGift, 0, len(user.Gifts.Received))
|
||||
for _, gift := range user.Gifts.Received {
|
||||
items = append(items, &pb.ReceivedGift{
|
||||
GiftCommon: toProtoGiftCommon(gift.GiftCommon),
|
||||
ReceivedDatetime: timestampOrNilGift(gift.ReceivedDatetime),
|
||||
})
|
||||
}
|
||||
return &pb.GetGiftReceiveHistoryListResponse{
|
||||
Gift: items,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toProtoGiftCommon(gift store.GiftCommonState) *pb.GiftCommon {
|
||||
return &pb.GiftCommon{
|
||||
PossessionType: gift.PossessionType,
|
||||
PossessionId: gift.PossessionId,
|
||||
Count: gift.Count,
|
||||
GrantDatetime: timestampOrNilGift(gift.GrantDatetime),
|
||||
DescriptionGiftTextId: gift.DescriptionGiftTextId,
|
||||
EquipmentData: gift.EquipmentData,
|
||||
}
|
||||
}
|
||||
|
||||
func timestampOrNilGift(unixMillis int64) *timestamppb.Timestamp {
|
||||
if unixMillis == 0 {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(time.UnixMilli(unixMillis))
|
||||
}
|
||||
|
||||
func pageCount(total, pageSize int) int32 {
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
return 1
|
||||
}
|
||||
pages := total / pageSize
|
||||
if total%pageSize != 0 {
|
||||
pages++
|
||||
}
|
||||
return int32(pages)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type GimmickServiceServer struct {
|
||||
pb.UnimplementedGimmickServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
gimmickCatalog *masterdata.GimmickCatalog
|
||||
}
|
||||
|
||||
func NewGimmickServiceServer(users store.UserRepository, sessions store.SessionRepository, gimmickCatalog *masterdata.GimmickCatalog) *GimmickServiceServer {
|
||||
return &GimmickServiceServer{users: users, sessions: sessions, gimmickCatalog: gimmickCatalog}
|
||||
}
|
||||
|
||||
func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.UpdateSequenceRequest) (*pb.UpdateSequenceResponse, error) {
|
||||
log.Printf("[GimmickService] UpdateSequence: scheduleId=%d sequenceId=%d",
|
||||
req.GimmickSequenceScheduleId, req.GimmickSequenceId)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
key := store.GimmickSequenceKey{
|
||||
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
||||
GimmickSequenceId: req.GimmickSequenceId,
|
||||
}
|
||||
sequence := user.Gimmick.Sequences[key]
|
||||
sequence.Key = key
|
||||
user.Gimmick.Sequences[key] = sequence
|
||||
})
|
||||
return &pb.UpdateSequenceResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickSequence"})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *pb.UpdateGimmickProgressRequest) (*pb.UpdateGimmickProgressResponse, error) {
|
||||
log.Printf("[GimmickService] UpdateGimmickProgress: scheduleId=%d sequenceId=%d gimmickId=%d ornamentIndex=%d progressValueBit=%d flowType=%d",
|
||||
req.GimmickSequenceScheduleId, req.GimmickSequenceId, req.GimmickId, req.GimmickOrnamentIndex, req.ProgressValueBit, req.FlowType)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
progressKey := store.GimmickKey{
|
||||
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
||||
GimmickSequenceId: req.GimmickSequenceId,
|
||||
GimmickId: req.GimmickId,
|
||||
}
|
||||
progress := user.Gimmick.Progress[progressKey]
|
||||
progress.Key = progressKey
|
||||
progress.StartDatetime = nowMillis
|
||||
user.Gimmick.Progress[progressKey] = progress
|
||||
|
||||
ornamentKey := store.GimmickOrnamentKey{
|
||||
GimmickSequenceScheduleId: req.GimmickSequenceScheduleId,
|
||||
GimmickSequenceId: req.GimmickSequenceId,
|
||||
GimmickId: req.GimmickId,
|
||||
GimmickOrnamentIndex: req.GimmickOrnamentIndex,
|
||||
}
|
||||
ornament := user.Gimmick.OrnamentProgress[ornamentKey]
|
||||
ornament.Key = ornamentKey
|
||||
ornament.ProgressValueBit = req.ProgressValueBit
|
||||
ornament.BaseDatetime = nowMillis
|
||||
user.Gimmick.OrnamentProgress[ornamentKey] = ornament
|
||||
})
|
||||
return &pb.UpdateGimmickProgressResponse{
|
||||
GimmickOrnamentReward: []*pb.GimmickReward{},
|
||||
IsSequenceCleared: false,
|
||||
GimmickSequenceClearReward: []*pb.GimmickReward{},
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserGimmick",
|
||||
"IUserGimmickOrnamentProgress",
|
||||
})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *emptypb.Empty) (*pb.InitSequenceScheduleResponse, error) {
|
||||
log.Printf("[GimmickService] InitSequenceSchedule")
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
now := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
added := 0
|
||||
for _, key := range s.gimmickCatalog.ActiveScheduleKeys(*user, now) {
|
||||
if _, exists := user.Gimmick.Sequences[key]; !exists {
|
||||
user.Gimmick.Sequences[key] = store.GimmickSequenceState{Key: key}
|
||||
added++
|
||||
}
|
||||
}
|
||||
if added > 0 {
|
||||
log.Printf("[GimmickService] InitSequenceSchedule: added %d sequences (total %d)", added, len(user.Gimmick.Sequences))
|
||||
}
|
||||
})
|
||||
return &pb.InitSequenceScheduleResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), gimmickDiffTables)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GimmickServiceServer) Unlock(ctx context.Context, req *pb.UnlockRequest) (*pb.UnlockResponse, error) {
|
||||
log.Printf("[GimmickService] Unlock: gimmickKeys=%d", len(req.GimmickKey))
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, item := range req.GimmickKey {
|
||||
key := store.GimmickKey{
|
||||
GimmickSequenceScheduleId: item.GimmickSequenceScheduleId,
|
||||
GimmickSequenceId: item.GimmickSequenceId,
|
||||
GimmickId: item.GimmickId,
|
||||
}
|
||||
unlock := user.Gimmick.Unlocks[key]
|
||||
unlock.Key = key
|
||||
unlock.IsUnlocked = true
|
||||
user.Gimmick.Unlocks[key] = unlock
|
||||
}
|
||||
})
|
||||
return &pb.UnlockResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickUnlock"})),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// listBinEntry holds path (')' as segment separator) and size from list.bin; Size is 0 when not present.
|
||||
type listBinEntry struct {
|
||||
Path string
|
||||
Size int64
|
||||
MD5 string
|
||||
}
|
||||
|
||||
// listBinIndex caches object_id → entry for a revision.
|
||||
type listBinIndex map[string]listBinEntry
|
||||
|
||||
type infoAlias struct {
|
||||
ToName string
|
||||
ToRevision string
|
||||
MD5 string
|
||||
}
|
||||
|
||||
type assetCandidate struct {
|
||||
Path string
|
||||
Revision string
|
||||
Source string
|
||||
ExpectedMD5 string
|
||||
}
|
||||
|
||||
type listBinLoad struct {
|
||||
done chan struct{}
|
||||
idx listBinIndex
|
||||
ok bool
|
||||
}
|
||||
|
||||
type infoLoad struct {
|
||||
done chan struct{}
|
||||
m map[string]infoAlias
|
||||
}
|
||||
|
||||
var (
|
||||
listBinCache = make(map[string]listBinIndex) // revision → index
|
||||
listBinInflight = make(map[string]*listBinLoad)
|
||||
listBinCacheMu sync.RWMutex
|
||||
infoCache = make(map[string]map[string]infoAlias) // revision → from-name → duplicate target
|
||||
infoInflight = make(map[string]*infoLoad)
|
||||
infoCacheMu sync.RWMutex
|
||||
)
|
||||
|
||||
// infoJSONEntry is one entry from assets/revisions/{rev}/info.json (duplicate files: serve to-name when asked for from-name).
|
||||
type infoJSONEntry struct {
|
||||
FromName string `json:"from-name"`
|
||||
ToName string `json:"to-name"`
|
||||
ToRevision *int `json:"to-revision"`
|
||||
MD5 string `json:"md5"`
|
||||
}
|
||||
|
||||
// readVarint reads a protobuf varint from b, returns value and number of bytes consumed.
|
||||
func readVarint(b []byte) (value int, n int) {
|
||||
for i := 0; i < len(b) && i < 10; i++ {
|
||||
value |= int(b[i]&0x7f) << (7 * i)
|
||||
n = i + 1
|
||||
if b[i]&0x80 == 0 {
|
||||
return value, n
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func skipProtoField(wireType int, data []byte, offset int) (int, bool) {
|
||||
switch wireType {
|
||||
case 0:
|
||||
_, n := readVarint(data[offset:])
|
||||
if n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return offset + n, true
|
||||
case 1:
|
||||
if offset+8 > len(data) {
|
||||
return 0, false
|
||||
}
|
||||
return offset + 8, true
|
||||
case 2:
|
||||
length, n := readVarint(data[offset:])
|
||||
if n == 0 || length < 0 || offset+n+length > len(data) {
|
||||
return 0, false
|
||||
}
|
||||
return offset + n + length, true
|
||||
case 5:
|
||||
if offset+4 > len(data) {
|
||||
return 0, false
|
||||
}
|
||||
return offset + 4, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseListBinEntry(data []byte) (objectId string, entry listBinEntry, ok bool) {
|
||||
i := 0
|
||||
for i < len(data) {
|
||||
tag, n := readVarint(data[i:])
|
||||
if n == 0 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
i += n
|
||||
fieldNum := tag >> 3
|
||||
wireType := tag & 0x7
|
||||
|
||||
switch fieldNum {
|
||||
case 3: // path
|
||||
if wireType != 2 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
length, vn := readVarint(data[i:])
|
||||
if vn == 0 || length < 0 || i+vn+length > len(data) {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
entry.Path = string(data[i+vn : i+vn+length])
|
||||
i += vn + length
|
||||
case 4: // size
|
||||
if wireType != 0 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
size, vn := readVarint(data[i:])
|
||||
if vn == 0 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
if size >= 256 {
|
||||
entry.Size = int64(size)
|
||||
}
|
||||
i += vn
|
||||
case 10: // md5
|
||||
if wireType != 2 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
length, vn := readVarint(data[i:])
|
||||
if vn == 0 || length < 0 || i+vn+length > len(data) {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
entry.MD5 = string(data[i+vn : i+vn+length])
|
||||
i += vn + length
|
||||
case 11: // object_id
|
||||
if wireType != 2 {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
length, vn := readVarint(data[i:])
|
||||
if vn == 0 || length <= 0 || i+vn+length > len(data) {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
objectId = string(data[i+vn : i+vn+length])
|
||||
i += vn + length
|
||||
default:
|
||||
next, ok := skipProtoField(wireType, data, i)
|
||||
if !ok {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
i = next
|
||||
}
|
||||
}
|
||||
|
||||
if objectId == "" || entry.Path == "" {
|
||||
return "", listBinEntry{}, false
|
||||
}
|
||||
return objectId, entry, true
|
||||
}
|
||||
|
||||
// parseListBin reads list.bin and builds object_id (6-byte string) → entry (path, size, md5).
|
||||
// The file is a protobuf message with repeated nested entry messages, so we parse each entry
|
||||
// boundary first instead of doing a flat scan across the whole file.
|
||||
func parseListBin(data []byte) listBinIndex {
|
||||
idx := make(listBinIndex)
|
||||
i := 0
|
||||
for i < len(data) {
|
||||
tag, n := readVarint(data[i:])
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
i += n
|
||||
wireType := tag & 0x7
|
||||
|
||||
if wireType == 2 {
|
||||
length, vn := readVarint(data[i:])
|
||||
if vn == 0 || length < 0 || i+vn+length > len(data) {
|
||||
break
|
||||
}
|
||||
entryBytes := data[i+vn : i+vn+length]
|
||||
objectId, entry, ok := parseListBinEntry(entryBytes)
|
||||
if ok {
|
||||
idx[objectId] = entry
|
||||
i += vn + length
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
next, ok := skipProtoField(wireType, data, i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
i = next
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func loadListBinIndex(revision string) (listBinIndex, bool) {
|
||||
listBinCacheMu.RLock()
|
||||
idx, ok := listBinCache[revision]
|
||||
listBinCacheMu.RUnlock()
|
||||
if ok {
|
||||
return idx, true
|
||||
}
|
||||
|
||||
listBinCacheMu.Lock()
|
||||
if idx, ok := listBinCache[revision]; ok {
|
||||
listBinCacheMu.Unlock()
|
||||
return idx, true
|
||||
}
|
||||
if load := listBinInflight[revision]; load != nil {
|
||||
listBinCacheMu.Unlock()
|
||||
<-load.done
|
||||
return load.idx, load.ok
|
||||
}
|
||||
load := &listBinLoad{done: make(chan struct{})}
|
||||
listBinInflight[revision] = load
|
||||
listBinCacheMu.Unlock()
|
||||
|
||||
filePath := filepath.Join("assets", "revisions", revision, "list.bin")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
listBinCacheMu.Lock()
|
||||
delete(listBinInflight, revision)
|
||||
close(load.done)
|
||||
listBinCacheMu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
idx = parseListBin(data)
|
||||
load.idx = idx
|
||||
load.ok = true
|
||||
listBinCacheMu.Lock()
|
||||
listBinCache[revision] = idx
|
||||
delete(listBinInflight, revision)
|
||||
close(load.done)
|
||||
listBinCacheMu.Unlock()
|
||||
return idx, true
|
||||
}
|
||||
|
||||
func loadInfoIndex(revision string) map[string]infoAlias {
|
||||
infoCacheMu.RLock()
|
||||
m, ok := infoCache[revision]
|
||||
infoCacheMu.RUnlock()
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
infoCacheMu.Lock()
|
||||
if m, ok := infoCache[revision]; ok {
|
||||
infoCacheMu.Unlock()
|
||||
return m
|
||||
}
|
||||
if load := infoInflight[revision]; load != nil {
|
||||
infoCacheMu.Unlock()
|
||||
<-load.done
|
||||
return load.m
|
||||
}
|
||||
load := &infoLoad{done: make(chan struct{})}
|
||||
infoInflight[revision] = load
|
||||
infoCacheMu.Unlock()
|
||||
|
||||
filePath := filepath.Join("assets", "revisions", revision, "info.json")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
infoCacheMu.Lock()
|
||||
infoCache[revision] = nil
|
||||
delete(infoInflight, revision)
|
||||
close(load.done)
|
||||
infoCacheMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
var entries []infoJSONEntry
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
infoCacheMu.Lock()
|
||||
infoCache[revision] = nil
|
||||
delete(infoInflight, revision)
|
||||
close(load.done)
|
||||
infoCacheMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
m = make(map[string]infoAlias)
|
||||
for _, e := range entries {
|
||||
if e.FromName != "" && e.ToName != "" {
|
||||
aliasRevision := revision
|
||||
if e.ToRevision != nil {
|
||||
aliasRevision = strconv.Itoa(*e.ToRevision)
|
||||
}
|
||||
m[e.FromName] = infoAlias{
|
||||
ToName: e.ToName,
|
||||
ToRevision: aliasRevision,
|
||||
MD5: e.MD5,
|
||||
}
|
||||
}
|
||||
}
|
||||
load.m = m
|
||||
infoCacheMu.Lock()
|
||||
infoCache[revision] = m
|
||||
delete(infoInflight, revision)
|
||||
close(load.done)
|
||||
infoCacheMu.Unlock()
|
||||
return m
|
||||
}
|
||||
|
||||
func pathStrToFullPaths(revision, assetType, pathStr string) []string {
|
||||
fsPath := strings.ReplaceAll(pathStr, ")", "/")
|
||||
if strings.Contains(fsPath, "..") || filepath.IsAbs(fsPath) || strings.HasPrefix(fsPath, "/") {
|
||||
return nil
|
||||
}
|
||||
fsPath = filepath.Clean(fsPath)
|
||||
if strings.Contains(fsPath, "..") {
|
||||
return nil
|
||||
}
|
||||
// Prefer "global" (en) when list.bin points to ja/ko: try en first, then original.
|
||||
var pathStrs []string
|
||||
if strings.Contains(pathStr, ")ja)") {
|
||||
pathStrs = append(pathStrs, strings.ReplaceAll(pathStr, ")ja)", ")en)"))
|
||||
}
|
||||
if strings.Contains(pathStr, ")ko)") {
|
||||
pathStrs = append(pathStrs, strings.ReplaceAll(pathStr, ")ko)", ")en)"))
|
||||
}
|
||||
pathStrs = append(pathStrs, pathStr)
|
||||
base := filepath.Join("assets", "revisions", revision)
|
||||
var out []string
|
||||
seen := make(map[string]bool)
|
||||
for _, p := range pathStrs {
|
||||
cleaned := filepath.Clean(strings.ReplaceAll(p, ")", "/"))
|
||||
if seen[cleaned] {
|
||||
continue
|
||||
}
|
||||
seen[cleaned] = true
|
||||
switch assetType {
|
||||
case "assetbundle":
|
||||
out = append(out, filepath.Join(base, "assetbundle", cleaned+".assetbundle"))
|
||||
case "resources":
|
||||
out = append(out, filepath.Join(base, "resources", cleaned))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendUniqueCandidate(candidates []assetCandidate, seen map[string]bool, candidate assetCandidate) []assetCandidate {
|
||||
key := candidate.Revision + ":" + candidate.Path
|
||||
if seen[key] {
|
||||
return candidates
|
||||
}
|
||||
seen[key] = true
|
||||
return append(candidates, candidate)
|
||||
}
|
||||
|
||||
func duplicateCandidatePath(candidate assetCandidate, assetType, targetRevision, targetBaseName string) string {
|
||||
root := filepath.Join("assets", "revisions", candidate.Revision, assetType)
|
||||
rel, err := filepath.Rel(root, candidate.Path)
|
||||
if err != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join("assets", "revisions", targetRevision, assetType, filepath.Dir(rel), targetBaseName)
|
||||
}
|
||||
|
||||
// objectIdToFilePathCandidates returns candidate file paths for the object: list.bin path, locale fallbacks
|
||||
// (ja/ko -> en), then info.json duplicate mappings (from-name -> to-name, possibly in a different revision).
|
||||
// Callers should try each path until one exists on disk.
|
||||
func objectIdToFilePathCandidates(revision, assetType, objectId string) (candidates []assetCandidate, size int64, ok bool) {
|
||||
idx, ok := loadListBinIndex(revision)
|
||||
if !ok || idx == nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
entry, ok := idx[objectId]
|
||||
if !ok || entry.Path == "" {
|
||||
return nil, 0, false
|
||||
}
|
||||
paths := pathStrToFullPaths(revision, assetType, entry.Path)
|
||||
if len(paths) == 0 {
|
||||
return nil, 0, false
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for _, path := range paths {
|
||||
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
||||
Path: path,
|
||||
Revision: revision,
|
||||
Source: "list.bin",
|
||||
ExpectedMD5: entry.MD5,
|
||||
})
|
||||
}
|
||||
// Add paths from info.json: when requested file is a "from-name" (duplicate not included), serve "to-name" instead.
|
||||
infoIndex := loadInfoIndex(revision)
|
||||
if len(infoIndex) > 0 {
|
||||
for _, c := range candidates {
|
||||
alias, ok := infoIndex[filepath.Base(c.Path)]
|
||||
if !ok || alias.ToName == "" {
|
||||
continue
|
||||
}
|
||||
alt := duplicateCandidatePath(c, assetType, alias.ToRevision, alias.ToName)
|
||||
if alt == "" {
|
||||
continue
|
||||
}
|
||||
candidates = appendUniqueCandidate(candidates, seen, assetCandidate{
|
||||
Path: alt,
|
||||
Revision: alias.ToRevision,
|
||||
Source: "info.json redirect",
|
||||
ExpectedMD5: alias.MD5,
|
||||
})
|
||||
}
|
||||
}
|
||||
return candidates, entry.Size, true
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type LoginBonusServiceServer struct {
|
||||
pb.UnimplementedLoginBonusServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.LoginBonusCatalog
|
||||
}
|
||||
|
||||
func NewLoginBonusServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.LoginBonusCatalog) *LoginBonusServiceServer {
|
||||
return &LoginBonusServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb.Empty) (*pb.ReceiveStampResponse, error) {
|
||||
log.Printf("[LoginBonusService] ReceiveStamp")
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
now := gametime.NowMillis()
|
||||
nextStamp := user.LoginBonus.CurrentStampNumber + 1
|
||||
|
||||
reward, ok := s.catalog.LookupStampReward(
|
||||
user.LoginBonus.LoginBonusId,
|
||||
user.LoginBonus.CurrentPageNumber,
|
||||
nextStamp,
|
||||
)
|
||||
if !ok {
|
||||
log.Fatalf("[LoginBonusService] no reward found for bonusId=%d page=%d stamp=%d",
|
||||
user.LoginBonus.LoginBonusId, user.LoginBonus.CurrentPageNumber, nextStamp)
|
||||
}
|
||||
|
||||
log.Printf("[LoginBonusService] stamp %d -> possType=%d possId=%d count=%d (-> gift box)",
|
||||
nextStamp, reward.PossessionType, reward.PossessionId, reward.Count)
|
||||
|
||||
user.Gifts.NotReceived = append(user.Gifts.NotReceived, store.NotReceivedGiftState{
|
||||
GiftCommon: store.GiftCommonState{
|
||||
PossessionType: reward.PossessionType,
|
||||
PossessionId: reward.PossessionId,
|
||||
Count: reward.Count,
|
||||
GrantDatetime: now,
|
||||
},
|
||||
ExpirationDatetime: now + int64(30*24*time.Hour/time.Millisecond),
|
||||
UserGiftUuid: fmt.Sprintf("login-bonus-%d-%d", userId, nextStamp),
|
||||
})
|
||||
user.Notifications.GiftNotReceiveCount = int32(len(user.Gifts.NotReceived))
|
||||
user.LoginBonus.CurrentStampNumber = nextStamp
|
||||
user.LoginBonus.LatestRewardReceiveDatetime = now
|
||||
user.LoginBonus.LatestVersion = now
|
||||
})
|
||||
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
|
||||
userdata.FullClientTableMap(user),
|
||||
[]string{"IUserLoginBonus"},
|
||||
))
|
||||
setCommonResponseTrailers(ctx, diff, false)
|
||||
return &pb.ReceiveStampResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
var materialDiffTables = []string{
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
}
|
||||
|
||||
type MaterialServiceServer struct {
|
||||
pb.UnimplementedMaterialServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.MaterialCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewMaterialServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.MaterialCatalog, config *masterdata.GameConfig) *MaterialServiceServer {
|
||||
return &MaterialServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRequest) (*pb.MaterialSellResponse, error) {
|
||||
log.Printf("[MaterialService] Sell: %d item(s)", len(req.MaterialPossession))
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
totalGold := int32(0)
|
||||
for _, item := range req.MaterialPossession {
|
||||
mat, ok := s.catalog.All[item.MaterialId]
|
||||
if !ok {
|
||||
log.Printf("[MaterialService] Sell: unknown materialId=%d, skipping", item.MaterialId)
|
||||
continue
|
||||
}
|
||||
|
||||
cur := user.Materials[item.MaterialId]
|
||||
if cur < item.Count {
|
||||
log.Printf("[MaterialService] Sell: insufficient materialId=%d have=%d need=%d", item.MaterialId, cur, item.Count)
|
||||
continue
|
||||
}
|
||||
|
||||
user.Materials[item.MaterialId] -= item.Count
|
||||
if user.Materials[item.MaterialId] <= 0 {
|
||||
delete(user.Materials, item.MaterialId)
|
||||
}
|
||||
|
||||
gold := mat.SellPrice * item.Count
|
||||
totalGold += gold
|
||||
log.Printf("[MaterialService] Sell: materialId=%d x%d -> %d gold", item.MaterialId, item.Count, gold)
|
||||
}
|
||||
|
||||
if totalGold > 0 {
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
||||
log.Printf("[MaterialService] Sell: total gold +%d", totalGold)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("material sell: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), materialDiffTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.MaterialSellResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type MissionServiceServer struct {
|
||||
pb.UnimplementedMissionServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewMissionServiceServer(users store.UserRepository, sessions store.SessionRepository) *MissionServiceServer {
|
||||
return &MissionServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *MissionServiceServer) UpdateMissionProgress(ctx context.Context, req *pb.UpdateMissionProgressRequest) (*pb.UpdateMissionProgressResponse, error) {
|
||||
log.Printf("[MissionService] UpdateMissionProgress: cage=%v pictureBook=%v", req.CageMeasurableValues, req.PictureBookMeasurableValues)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
snapshot, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMission"}))
|
||||
|
||||
return &pb.UpdateMissionProgressResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type MovieServiceServer struct {
|
||||
pb.UnimplementedMovieServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewMovieServiceServer(users store.UserRepository, sessions store.SessionRepository) *MovieServiceServer {
|
||||
return &MovieServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *MovieServiceServer) SaveViewedMovie(ctx context.Context, req *pb.SaveViewedMovieRequest) (*pb.SaveViewedMovieResponse, error) {
|
||||
log.Printf("[MovieService] SaveViewedMovie: movieIds=%v", req.MovieId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
now := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, mid := range req.MovieId {
|
||||
user.ViewedMovies[mid] = now
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMovie"}))
|
||||
|
||||
return &pb.SaveViewedMovieResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type NaviCutInServiceServer struct {
|
||||
pb.UnimplementedNaviCutInServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewNaviCutInServiceServer(users store.UserRepository, sessions store.SessionRepository) *NaviCutInServiceServer {
|
||||
return &NaviCutInServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *NaviCutInServiceServer) RegisterPlayed(ctx context.Context, req *pb.RegisterPlayedRequest) (*pb.RegisterPlayedResponse, error) {
|
||||
log.Printf("[NaviCutInService] RegisterPlayed: naviCutId=%d", req.NaviCutId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.NaviCutInPlayed[req.NaviCutId] = true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserNaviCutIn"}))
|
||||
|
||||
return &pb.RegisterPlayedResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type NotificationServiceServer struct {
|
||||
pb.UnimplementedNotificationServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewNotificationServiceServer(users store.UserRepository, sessions store.SessionRepository) *NotificationServiceServer {
|
||||
return &NotificationServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *NotificationServiceServer) GetHeaderNotification(ctx context.Context, req *emptypb.Empty) (*pb.GetHeaderNotificationResponse, error) {
|
||||
log.Printf("[NotificationService] GetHeaderNotification")
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return &pb.GetHeaderNotificationResponse{
|
||||
GiftNotReceiveCount: 0,
|
||||
FriendRequestReceiveCount: 0,
|
||||
IsExistUnreadInformation: false,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
return &pb.GetHeaderNotificationResponse{
|
||||
GiftNotReceiveCount: int32(len(user.Gifts.NotReceived)),
|
||||
FriendRequestReceiveCount: user.Notifications.FriendRequestReceiveCount,
|
||||
IsExistUnreadInformation: user.Notifications.IsExistUnreadInformation,
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const termsVersionMarker = "###123###"
|
||||
const privacyVersionMarker = "###123###"
|
||||
|
||||
const informationPage = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Lunar Tear</title>
|
||||
<style>
|
||||
body { margin:0; padding:40px 20px; font-family:"Noto Sans",sans-serif;
|
||||
background:#0a0a0f; color:#d4cfc6; text-align:center; }
|
||||
h1 { font-size:1.4em; letter-spacing:.15em; color:#e8e0d0; margin-bottom:4px; }
|
||||
.sub { font-size:.75em; color:#888; margin-bottom:32px; }
|
||||
.sep { width:60px; border:none; border-top:1px solid #333; margin:24px auto; }
|
||||
p { font-size:.85em; line-height:1.6; color:#999; max-width:360px; margin:0 auto 12px; }
|
||||
a { color:#a0c4e8; text-decoration:none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>LUNAR TEAR</h1>
|
||||
<div class="sub">Private Preservation Server</div>
|
||||
<hr class="sep">
|
||||
<p>A community effort to keep NieR Re[in]carnation playable after official service ended.</p>
|
||||
<p>This server is not affiliated with or endorsed by SQUARE ENIX or Applibot.</p>
|
||||
<hr class="sep">
|
||||
<p style="font-size:.7em;color:#666;">© SQUARE ENIX / Applibot — All game assets belong to their respective owners.</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// resourcesURLOriginal is the base URL embedded in list.bin; must be replaced with same-length (43 bytes) when rewriting.
|
||||
const resourcesURLOriginal = "https://resources.app.nierreincarnation.com"
|
||||
|
||||
type OctoHTTPServer struct {
|
||||
mux *http.ServeMux
|
||||
ResourcesBaseURL string // if non-empty and exactly 43 chars, list.bin is rewritten to use this base for asset URLs
|
||||
revisions *revisionTracker
|
||||
resolver *assetResolver
|
||||
}
|
||||
|
||||
func staticPageLanguage(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
for i := 0; i+1 < len(parts); i++ {
|
||||
if parts[i] == "static" && parts[i+1] != "" {
|
||||
return parts[i+1]
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func renderStaticTermsPage(title, language, version string) string {
|
||||
return "<html><head><title>" + title + "</title></head><body><h1>" + title +
|
||||
"</h1><p>Language: " + language + "</p><p>Version: " + version + "</p></body></html>"
|
||||
}
|
||||
|
||||
// countResponseWriter wraps http.ResponseWriter and counts bytes written.
|
||||
type countResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
n int64
|
||||
}
|
||||
|
||||
type fileMD5Entry struct {
|
||||
size int64
|
||||
modTime int64
|
||||
md5 string
|
||||
}
|
||||
|
||||
var (
|
||||
fileMD5Cache = make(map[string]fileMD5Entry)
|
||||
fileMD5CacheMu sync.RWMutex
|
||||
)
|
||||
|
||||
func (c *countResponseWriter) Write(p []byte) (int, error) {
|
||||
n, err := c.ResponseWriter.Write(p)
|
||||
c.n += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func fileMD5Hex(path string, info os.FileInfo) (string, error) {
|
||||
modTime := info.ModTime().UnixNano()
|
||||
|
||||
fileMD5CacheMu.RLock()
|
||||
cached, ok := fileMD5Cache[path]
|
||||
fileMD5CacheMu.RUnlock()
|
||||
if ok && cached.size == info.Size() && cached.modTime == modTime {
|
||||
return cached.md5, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
fileMD5CacheMu.Lock()
|
||||
fileMD5Cache[path] = fileMD5Entry{
|
||||
size: info.Size(),
|
||||
modTime: modTime,
|
||||
md5: sum,
|
||||
}
|
||||
fileMD5CacheMu.Unlock()
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func NewOctoHTTPServer(resourcesBaseURL string) *OctoHTTPServer {
|
||||
s := &OctoHTTPServer{
|
||||
mux: http.NewServeMux(),
|
||||
ResourcesBaseURL: resourcesBaseURL,
|
||||
revisions: newRevisionTracker(),
|
||||
resolver: newAssetResolver(),
|
||||
}
|
||||
s.mux.HandleFunc("/", s.handleAll)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *OctoHTTPServer) Handler() http.Handler {
|
||||
return s.mux
|
||||
}
|
||||
|
||||
func (s *OctoHTTPServer) handleAll(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
isAssetRequest := strings.Contains(path, "/unso-")
|
||||
isMasterDataRequest := strings.Contains(path, "/assets/release/") && strings.Contains(path, "database.bin")
|
||||
if !isAssetRequest && !isMasterDataRequest {
|
||||
log.Printf("[HTTP] %s %s (Host: %s)", r.Method, r.URL.String(), r.Host)
|
||||
for k, v := range r.Header {
|
||||
log.Printf("[HTTP] %s: %s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Octo v2 API — asset bundle management
|
||||
if strings.HasPrefix(path, "/v2/") {
|
||||
s.handleOctoV2(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
// Octo v1 list: /v1/list/{version}/{revision} — same list.bin as v2, keyed by revision
|
||||
if strings.HasPrefix(path, "/v1/list/") {
|
||||
s.serveOctoV1List(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
// Game web API requests
|
||||
if strings.Contains(path, "/web/") || strings.Contains(r.Host, "web.app.nierreincarnation") {
|
||||
s.handleWebAPI(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
// Master data download (should not be reached if version matches)
|
||||
if strings.HasPrefix(path, "/master-data/") {
|
||||
log.Printf("[HTTP] Master data request for path: %s — returning empty", path)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
// /assets/release/{version}/database.bin.e — master data (HEAD/GET), same as MariesWonderland
|
||||
if strings.Contains(path, "/assets/release/") && strings.Contains(path, "database.bin") {
|
||||
s.serveDatabaseBinE(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
// Asset bundle requests (from list.bin URLs: .../unso-{v}-{type}/{o}?generation=...&alt=media)
|
||||
if strings.Contains(path, "/unso-") {
|
||||
s.serveUnsoAsset(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
// In-game information / news page
|
||||
if strings.Contains(path, "/information") {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(informationPage))
|
||||
return
|
||||
}
|
||||
|
||||
// Log request body for debugging Octo protocol
|
||||
if r.Body != nil {
|
||||
body := make([]byte, 4096)
|
||||
n, _ := r.Body.Read(body)
|
||||
if n > 0 {
|
||||
log.Printf("[HTTP] body (%d bytes): %x", n, body[:n])
|
||||
if n < 256 {
|
||||
log.Printf("[HTTP] body (ascii): %s", string(body[:n]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[HTTP] >>> UNHANDLED REQUEST: %s %s — returning empty 200", r.Method, path)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte{})
|
||||
}
|
||||
|
||||
func (s *OctoHTTPServer) handleOctoV2(w http.ResponseWriter, r *http.Request, path string) {
|
||||
log.Printf("[OctoV2] %s %s", r.Method, path)
|
||||
|
||||
// /v2/pub/a/{appId}/v/{version}/list/{offset} — resource listing
|
||||
if strings.Contains(path, "/list/") {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 {
|
||||
requestedRevision := parts[len(parts)-1]
|
||||
if requestedRevision != "" {
|
||||
revision := "0"
|
||||
filePath := "assets/revisions/0/list.bin"
|
||||
if requestedRevision != revision {
|
||||
log.Printf("[OctoV2] Resource list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
||||
}
|
||||
log.Printf("[OctoV2] Resource list request — serving %s (requested_revision=%s canonical_revision=%s)", filePath, requestedRevision, revision)
|
||||
s.revisions.Remember(r.RemoteAddr, revision)
|
||||
go s.resolver.Prewarm(revision)
|
||||
s.serveListBin(w, filePath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[OctoV2] Resource list request without revision segment — returning empty protobuf")
|
||||
w.Header().Set("Content-Type", "application/x-protobuf")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// /v2/pub/a/{appId}/v/{version}/info — DB info
|
||||
if strings.Contains(path, "/info") {
|
||||
log.Printf("[OctoV2] Info request — returning empty protobuf")
|
||||
w.Header().Set("Content-Type", "application/x-protobuf")
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[OctoV2] Unknown endpoint: %s — returning empty protobuf", path)
|
||||
w.Header().Set("Content-Type", "application/x-protobuf")
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
// serveOctoV1List handles GET /v1/list/{version}/{revision} — serves assets/revisions/{revision}/list.bin.
|
||||
func (s *OctoHTTPServer) serveOctoV1List(w http.ResponseWriter, r *http.Request, path string) {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
// ["v1", "list", "300116832", "0"] -> revision = last segment
|
||||
requestedRevision := "0"
|
||||
if len(parts) >= 4 {
|
||||
requestedRevision = parts[len(parts)-1]
|
||||
}
|
||||
revision := "0"
|
||||
filePath := "assets/revisions/0/list.bin"
|
||||
if requestedRevision != revision {
|
||||
log.Printf("[OctoV1] list request revision=%s canonicalized to revision=%s", requestedRevision, revision)
|
||||
}
|
||||
log.Printf("[OctoV1] %s %s — serving %s (requested_revision=%s canonical_revision=%s)", r.Method, path, filePath, requestedRevision, revision)
|
||||
s.revisions.Remember(r.RemoteAddr, revision)
|
||||
go s.resolver.Prewarm(revision)
|
||||
s.serveListBin(w, filePath)
|
||||
}
|
||||
|
||||
// serveUnsoAsset serves asset bundle or resource for URLs like /resource-bundle-server/unso-{version}-{type}/{object_id}.
|
||||
func (s *OctoHTTPServer) serveUnsoAsset(w http.ResponseWriter, r *http.Request, path string) {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
var segment, objectId string
|
||||
for i, p := range parts {
|
||||
if strings.HasPrefix(p, "unso-") && i+1 < len(parts) {
|
||||
segment = p
|
||||
objectId = parts[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if segment == "" || objectId == "" {
|
||||
log.Printf("[HTTP] Asset request malformed: %s", path)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// segment = "unso-200116832-assetbundle" -> type = last part after "-"
|
||||
segParts := strings.Split(segment, "-")
|
||||
if len(segParts) < 2 {
|
||||
log.Printf("[HTTP] Asset request segment malformed: %s", segment)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
assetType := segParts[len(segParts)-1] // "assetbundle" or "resources"
|
||||
if assetType != "assetbundle" && assetType != "resources" {
|
||||
log.Printf("[HTTP] Asset request unknown type: %s", assetType)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
activeRevision := s.revisions.Active(r.RemoteAddr)
|
||||
resolution, ok := s.resolver.Resolve(objectId, assetType, activeRevision)
|
||||
if !ok {
|
||||
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s active_revision=%s) no candidates", path, objectId, assetType, activeRevision)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
baseDir := filepath.Join("assets", "revisions")
|
||||
var triedPaths []string
|
||||
var md5Mismatches []string
|
||||
for _, candidate := range resolution.Candidates {
|
||||
rel, err := filepath.Rel(baseDir, candidate.Path)
|
||||
if err != nil || strings.Contains(rel, "..") || filepath.IsAbs(rel) {
|
||||
continue
|
||||
}
|
||||
triedPaths = append(triedPaths, candidate.Revision+":"+candidate.Path+" ["+candidate.Source+"]")
|
||||
f, err := os.Open(candidate.Path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
// Only validate size when list.bin gave a plausible file size (>= 256); small values are often wrong (e.g. different proto field).
|
||||
if resolution.ListSize >= 256 && info.Size() != resolution.ListSize {
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
if candidate.ExpectedMD5 != "" {
|
||||
actualMD5, err := fileMD5Hex(candidate.Path, info)
|
||||
if err != nil {
|
||||
log.Printf("[HTTP] Asset md5 read failed: %s err=%v", candidate.Path, err)
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(actualMD5, candidate.ExpectedMD5) {
|
||||
md5Mismatches = append(md5Mismatches, candidate.Revision+":"+candidate.Path+" ["+candidate.Source+"] expected="+candidate.ExpectedMD5+" actual="+actualMD5)
|
||||
log.Printf("[HTTP] Asset md5 mismatch: object_id=%s type=%s path=%s expected=%s actual=%s active_revision=%s list_revision=%s resolved_revision=%s source=%s", objectId, assetType, candidate.Path, candidate.ExpectedMD5, actualMD5, resolution.ActiveRevision, resolution.ListRevision, candidate.Revision, candidate.Source)
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
cw := &countResponseWriter{ResponseWriter: w}
|
||||
http.ServeContent(cw, r, filepath.Base(candidate.Path), info.ModTime(), f)
|
||||
return
|
||||
}
|
||||
if len(md5Mismatches) > 0 {
|
||||
log.Printf("[HTTP] Asset md5 mismatches: object_id=%s type=%s active_revision=%s list_revision=%s mismatches=%v", objectId, assetType, resolution.ActiveRevision, resolution.ListRevision, md5Mismatches)
|
||||
}
|
||||
log.Printf("[HTTP] Asset not found: %s (object_id=%s type=%s active_revision=%s list_revision=%s) tried paths: %v", path, objectId, assetType, resolution.ActiveRevision, resolution.ListRevision, triedPaths)
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// serveListBin reads list.bin from filePath, optionally rewrites the resource base URL to s.ResourcesBaseURL
|
||||
// (must be exactly 43 bytes to preserve protobuf layout), and writes the result to w.
|
||||
func (s *OctoHTTPServer) serveListBin(w http.ResponseWriter, filePath string) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Printf("[Octo] list.bin read error: %v", err)
|
||||
http.Error(w, "list not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
orig := []byte(resourcesURLOriginal)
|
||||
if s.ResourcesBaseURL != "" {
|
||||
if len(s.ResourcesBaseURL) != len(orig) {
|
||||
log.Printf("[Octo] resources-base-url length is %d, need %d — serving list.bin unchanged", len(s.ResourcesBaseURL), len(orig))
|
||||
} else {
|
||||
repl := []byte(s.ResourcesBaseURL)
|
||||
if idx := bytes.Index(data, orig); idx >= 0 {
|
||||
copy(data[idx:], repl)
|
||||
log.Printf("[Octo] list.bin: rewrote resource base URL to %s", s.ResourcesBaseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-protobuf")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// serveDatabaseBinE serves MasterMemory database: /assets/release/{version}/database.bin.e
|
||||
// -> assets/release/{version}.bin.e (or assets/release/database.bin.e fallback).
|
||||
func (s *OctoHTTPServer) serveDatabaseBinE(w http.ResponseWriter, r *http.Request, path string) {
|
||||
parts := strings.Split(path, "/")
|
||||
var version string
|
||||
for i, p := range parts {
|
||||
if p == "release" && i+1 < len(parts) {
|
||||
version = parts[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
filePath := "assets/release/database.bin.e"
|
||||
if version != "" {
|
||||
vPath := "assets/release/" + version + ".bin.e"
|
||||
if _, err := os.Stat(vPath); err == nil {
|
||||
filePath = vPath
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
func (s *OctoHTTPServer) handleWebAPI(w http.ResponseWriter, r *http.Request, path string) {
|
||||
log.Printf("[WebAPI] Serving: %s", path)
|
||||
|
||||
if strings.Contains(path, "database.bin") {
|
||||
s.serveDatabaseBinE(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "termsofuse") {
|
||||
language := staticPageLanguage(path)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(renderStaticTermsPage("Terms of Service", language, termsVersionMarker)))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "privacy") {
|
||||
language := staticPageLanguage(path)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(renderStaticTermsPage("Privacy Policy", language, privacyVersionMarker)))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "maintenance") {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body></body></html>`))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body></body></html>`))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type OmikujiServiceServer struct {
|
||||
pb.UnimplementedOmikujiServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.OmikujiCatalog
|
||||
}
|
||||
|
||||
func NewOmikujiServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.OmikujiCatalog) *OmikujiServiceServer {
|
||||
return &OmikujiServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiDrawRequest) (*pb.OmikujiDrawResponse, error) {
|
||||
log.Printf("[OmikujiService] OmikujiDraw: omikujiId=%d", req.OmikujiId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
now := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.DrawnOmikuji[req.OmikujiId] = now
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update user: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserOmikuji"}))
|
||||
|
||||
return &pb.OmikujiDrawResponse{
|
||||
OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId),
|
||||
OmikujiItem: []*pb.OmikujiItem{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
const partsMaxLevel = int32(15)
|
||||
|
||||
var partsDiffTables = []string{
|
||||
"IUserParts",
|
||||
"IUserConsumableItem",
|
||||
}
|
||||
|
||||
type PartsServiceServer struct {
|
||||
pb.UnimplementedPartsServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.PartsCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewPartsServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.PartsCatalog, config *masterdata.GameConfig) *PartsServiceServer {
|
||||
return &PartsServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest) (*pb.PartsSellResponse, error) {
|
||||
log.Printf("[PartsService] Sell: %d part(s)", len(req.UserPartsUuid))
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserParts", oldUser, userdata.SortedPartsRecords, []string{"userId", "userPartsUuid"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
totalGold := int32(0)
|
||||
for _, uuid := range req.UserPartsUuid {
|
||||
part, ok := user.Parts[uuid]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Sell: uuid=%s not found, skipping", uuid)
|
||||
continue
|
||||
}
|
||||
if part.IsProtected {
|
||||
log.Printf("[PartsService] Sell: uuid=%s is protected, skipping", uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
partDef, ok := s.catalog.PartsById[part.PartsId]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Sell: partsId=%d not in catalog, skipping", part.PartsId)
|
||||
continue
|
||||
}
|
||||
|
||||
sellFunc, ok := s.catalog.SellPriceByRarity[partDef.RarityType]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Sell: no sell price func for rarity=%d, skipping", partDef.RarityType)
|
||||
continue
|
||||
}
|
||||
|
||||
gold := sellFunc.Evaluate(part.Level)
|
||||
totalGold += gold
|
||||
delete(user.Parts, uuid)
|
||||
log.Printf("[PartsService] Sell: uuid=%s partsId=%d level=%d -> %d gold", uuid, part.PartsId, part.Level, gold)
|
||||
}
|
||||
|
||||
if totalGold > 0 {
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
||||
log.Printf("[PartsService] Sell: total gold +%d", totalGold)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parts sell: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), partsDiffTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.PartsSellResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRequest) (*pb.PartsEnhanceResponse, error) {
|
||||
log.Printf("[PartsService] Enhance: uuid=%s", req.UserPartsUuid)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
isSuccess := false
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
part, ok := user.Parts[req.UserPartsUuid]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Enhance: part uuid=%s not found", req.UserPartsUuid)
|
||||
return
|
||||
}
|
||||
|
||||
if part.Level >= partsMaxLevel {
|
||||
log.Printf("[PartsService] Enhance: part uuid=%s already at max level %d", req.UserPartsUuid, part.Level)
|
||||
return
|
||||
}
|
||||
|
||||
partDef, ok := s.catalog.PartsById[part.PartsId]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Enhance: part master id=%d not found", part.PartsId)
|
||||
return
|
||||
}
|
||||
|
||||
rarity, ok := s.catalog.RarityByRarityType[partDef.RarityType]
|
||||
if !ok {
|
||||
log.Printf("[PartsService] Enhance: rarity type=%d not found", partDef.RarityType)
|
||||
return
|
||||
}
|
||||
|
||||
goldCost := int32(0)
|
||||
if prices, ok := s.catalog.PriceByGroupAndLevel[rarity.PartsLevelUpPriceGroupId]; ok {
|
||||
goldCost = prices[part.Level]
|
||||
}
|
||||
|
||||
currentGold := user.ConsumableItems[s.config.ConsumableItemIdForGold]
|
||||
if currentGold < goldCost {
|
||||
log.Printf("[PartsService] Enhance: insufficient gold have=%d need=%d", currentGold, goldCost)
|
||||
return
|
||||
}
|
||||
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
|
||||
successRate := int32(1000)
|
||||
if rates, ok := s.catalog.RateByGroupAndLevel[rarity.PartsLevelUpRateGroupId]; ok {
|
||||
if r, ok := rates[part.Level]; ok {
|
||||
successRate = r
|
||||
}
|
||||
}
|
||||
|
||||
if rand.Intn(1000) < int(successRate) {
|
||||
part.Level++
|
||||
isSuccess = true
|
||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level-1, part.Level, successRate, goldCost)
|
||||
} else {
|
||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
||||
part.PartsId, part.Level, successRate, goldCost)
|
||||
}
|
||||
|
||||
part.LatestVersion = nowMillis
|
||||
user.Parts[req.UserPartsUuid] = part
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parts enhance: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, partsDiffTables))
|
||||
|
||||
return &pb.PartsEnhanceResponse{
|
||||
IsSuccess: isSuccess,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsReplacePresetRequest) (*pb.PartsReplacePresetResponse, error) {
|
||||
log.Printf("[PartsService] ReplacePreset: preset=%d uuids=[%s, %s, %s]",
|
||||
req.UserPartsPresetNumber, req.UserPartsUuid01, req.UserPartsUuid02, req.UserPartsUuid03)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
preset := user.PartsPresets[req.UserPartsPresetNumber]
|
||||
preset.UserPartsPresetNumber = req.UserPartsPresetNumber
|
||||
preset.UserPartsUuid01 = req.UserPartsUuid01
|
||||
preset.UserPartsUuid02 = req.UserPartsUuid02
|
||||
preset.UserPartsUuid03 = req.UserPartsUuid03
|
||||
preset.LatestVersion = nowMillis
|
||||
user.PartsPresets[req.UserPartsPresetNumber] = preset
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parts replace preset: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserPartsPreset"}))
|
||||
|
||||
return &pb.PartsReplacePresetResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type PortalCageServiceServer struct {
|
||||
pb.UnimplementedPortalCageServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewPortalCageServiceServer(users store.UserRepository, sessions store.SessionRepository) *PortalCageServiceServer {
|
||||
return &PortalCageServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Context, req *pb.UpdatePortalCageSceneProgressRequest) (*pb.UpdatePortalCageSceneProgressResponse, error) {
|
||||
log.Printf("[PortalCageService] UpdatePortalCageSceneProgress: portalCageSceneId=%d", req.PortalCageSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
now := gametime.NowMillis()
|
||||
user.PortalCageStatus.IsCurrentProgress = true
|
||||
user.PortalCageStatus.LatestVersion = now
|
||||
})
|
||||
|
||||
tables := userdata.SelectTables(
|
||||
userdata.FullClientTableMap(user),
|
||||
[]string{"IUserPortalCageStatus"},
|
||||
)
|
||||
return &pb.UpdatePortalCageSceneProgressResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTablesOrdered(tables, []string{"IUserPortalCageStatus"}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type BigHuntServiceServer struct {
|
||||
pb.UnimplementedBigHuntServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.BigHuntCatalog
|
||||
engine *questflow.QuestHandler
|
||||
}
|
||||
|
||||
func NewBigHuntServiceServer(
|
||||
users store.UserRepository,
|
||||
sessions store.SessionRepository,
|
||||
catalog *masterdata.BigHuntCatalog,
|
||||
engine *questflow.QuestHandler,
|
||||
) *BigHuntServiceServer {
|
||||
return &BigHuntServiceServer{users: users, sessions: sessions, catalog: catalog, engine: engine}
|
||||
}
|
||||
|
||||
var bigHuntDiffTables = []string{
|
||||
"IUserBigHuntProgressStatus",
|
||||
"IUserBigHuntMaxScore",
|
||||
"IUserBigHuntStatus",
|
||||
"IUserBigHuntScheduleMaxScore",
|
||||
"IUserBigHuntWeeklyMaxScore",
|
||||
"IUserBigHuntWeeklyStatus",
|
||||
}
|
||||
|
||||
func buildBigHuntDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
|
||||
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) StartBigHuntQuest(ctx context.Context, req *pb.StartBigHuntQuestRequest) (*pb.StartBigHuntQuestResponse, error) {
|
||||
log.Printf("[BigHuntService] StartBigHuntQuest: bossQuestId=%d questId=%d deckNumber=%d isDryRun=%v",
|
||||
req.BigHuntBossQuestId, req.BigHuntQuestId, req.UserDeckNumber, req.IsDryRun)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
bhQuest, ok := s.catalog.QuestById[req.BigHuntQuestId]
|
||||
if !ok {
|
||||
log.Printf("[BigHuntService] StartBigHuntQuest: unknown bigHuntQuestId=%d", req.BigHuntQuestId)
|
||||
}
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if ok {
|
||||
s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, req.UserDeckNumber, nowMillis)
|
||||
}
|
||||
|
||||
user.BigHuntProgress = store.BigHuntProgress{
|
||||
CurrentBigHuntBossQuestId: req.BigHuntBossQuestId,
|
||||
CurrentBigHuntQuestId: req.BigHuntQuestId,
|
||||
CurrentQuestSceneId: 0,
|
||||
IsDryRun: req.IsDryRun,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
|
||||
user.BigHuntDeckNumber = req.UserDeckNumber
|
||||
|
||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||
st.DailyChallengeCount++
|
||||
st.LatestChallengeDatetime = nowMillis
|
||||
st.LatestVersion = nowMillis
|
||||
user.BigHuntStatuses[req.BigHuntBossQuestId] = st
|
||||
})
|
||||
|
||||
return &pb.StartBigHuntQuestResponse{
|
||||
DiffUserData: buildBigHuntDiff(user, append([]string{"IUserQuest"}, bigHuntDiffTables...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) UpdateBigHuntQuestSceneProgress(ctx context.Context, req *pb.UpdateBigHuntQuestSceneProgressRequest) (*pb.UpdateBigHuntQuestSceneProgressResponse, error) {
|
||||
log.Printf("[BigHuntService] UpdateBigHuntQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.BigHuntProgress.CurrentQuestSceneId = req.QuestSceneId
|
||||
user.BigHuntProgress.LatestVersion = nowMillis
|
||||
})
|
||||
|
||||
return &pb.UpdateBigHuntQuestSceneProgressResponse{
|
||||
DiffUserData: buildBigHuntDiff(user, []string{"IUserBigHuntProgressStatus"}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) FinishBigHuntQuest(ctx context.Context, req *pb.FinishBigHuntQuestRequest) (*pb.FinishBigHuntQuestResponse, error) {
|
||||
log.Printf("[BigHuntService] FinishBigHuntQuest: bossQuestId=%d questId=%d isRetired=%v",
|
||||
req.BigHuntBossQuestId, req.BigHuntQuestId, req.IsRetired)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
bhQuest := s.catalog.QuestById[req.BigHuntQuestId]
|
||||
bossQuest := s.catalog.BossQuestById[req.BigHuntBossQuestId]
|
||||
boss := s.catalog.BossByBossId[bossQuest.BigHuntBossId]
|
||||
|
||||
var scoreInfo *pb.BigHuntScoreInfo
|
||||
var scoreRewards []*pb.BigHuntReward
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleBigHuntQuestFinish(user, bhQuest.QuestId, req.IsRetired, false, nowMillis)
|
||||
|
||||
if req.IsRetired || user.BigHuntProgress.IsDryRun {
|
||||
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
||||
return
|
||||
}
|
||||
|
||||
detail := user.BigHuntBattleDetail
|
||||
totalDamage := detail.TotalDamage
|
||||
baseScore := totalDamage
|
||||
|
||||
difficultyBonusPermil := int32(0)
|
||||
if coeff, ok := s.catalog.ScoreCoefficients[bhQuest.BigHuntQuestScoreCoefficientId]; ok {
|
||||
difficultyBonusPermil = coeff
|
||||
}
|
||||
|
||||
aliveBonusPermil := int32(500)
|
||||
|
||||
maxComboBonusPermil := int32(0)
|
||||
if detail.MaxComboCount >= 100 {
|
||||
maxComboBonusPermil = 300
|
||||
} else if detail.MaxComboCount >= 50 {
|
||||
maxComboBonusPermil = 200
|
||||
} else if detail.MaxComboCount >= 20 {
|
||||
maxComboBonusPermil = 100
|
||||
}
|
||||
|
||||
userScore := baseScore * int64(1000+difficultyBonusPermil+aliveBonusPermil+maxComboBonusPermil) / 1000
|
||||
|
||||
isHighScore := false
|
||||
oldMaxBoss := user.BigHuntMaxScores[bossQuest.BigHuntBossId]
|
||||
oldMax := oldMaxBoss.MaxScore
|
||||
if userScore > oldMax {
|
||||
isHighScore = true
|
||||
user.BigHuntMaxScores[bossQuest.BigHuntBossId] = store.BigHuntMaxScore{
|
||||
MaxScore: userScore,
|
||||
MaxScoreUpdateDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
schedKey := store.BigHuntScheduleScoreKey{
|
||||
BigHuntScheduleId: s.catalog.ActiveScheduleId,
|
||||
BigHuntBossId: bossQuest.BigHuntBossId,
|
||||
}
|
||||
oldSchedMax := user.BigHuntScheduleMaxScores[schedKey].MaxScore
|
||||
if userScore > oldSchedMax {
|
||||
user.BigHuntScheduleMaxScores[schedKey] = store.BigHuntScheduleMaxScore{
|
||||
MaxScore: userScore,
|
||||
MaxScoreUpdateDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
||||
weekKey := store.BigHuntWeeklyScoreKey{
|
||||
BigHuntWeeklyVersion: weeklyVersion,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
oldWeeklyMax := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
||||
if userScore > oldWeeklyMax {
|
||||
user.BigHuntWeeklyMaxScores[weekKey] = store.BigHuntWeeklyMaxScore{
|
||||
MaxScore: userScore,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
assetGradeIconId := s.catalog.ResolveGradeIconId(bossQuest.BigHuntBossId, userScore)
|
||||
|
||||
scoreInfo = &pb.BigHuntScoreInfo{
|
||||
UserScore: userScore,
|
||||
IsHighScore: isHighScore,
|
||||
TotalDamage: totalDamage,
|
||||
BaseScore: baseScore,
|
||||
DifficultyBonusPermil: difficultyBonusPermil,
|
||||
AliveBonusPermil: aliveBonusPermil,
|
||||
MaxComboBonusPermil: maxComboBonusPermil,
|
||||
AssetGradeIconId: assetGradeIconId,
|
||||
}
|
||||
|
||||
if isHighScore {
|
||||
rewardGroupId := s.catalog.ResolveActiveScoreRewardGroupId(
|
||||
bossQuest.BigHuntScoreRewardGroupScheduleId, nowMillis)
|
||||
if rewardGroupId > 0 {
|
||||
newItems := s.catalog.CollectNewRewards(rewardGroupId, oldMax, userScore)
|
||||
for _, item := range newItems {
|
||||
s.engine.Granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||
scoreRewards = append(scoreRewards, &pb.BigHuntReward{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.BigHuntProgress = store.BigHuntProgress{LatestVersion: nowMillis}
|
||||
user.BigHuntBattleBinary = nil
|
||||
user.BigHuntBattleDetail = store.BigHuntBattleDetail{}
|
||||
})
|
||||
|
||||
if scoreInfo == nil {
|
||||
scoreInfo = &pb.BigHuntScoreInfo{}
|
||||
}
|
||||
if scoreRewards == nil {
|
||||
scoreRewards = []*pb.BigHuntReward{}
|
||||
}
|
||||
|
||||
return &pb.FinishBigHuntQuestResponse{
|
||||
ScoreInfo: scoreInfo,
|
||||
ScoreReward: scoreRewards,
|
||||
BattleReport: &pb.BigHuntBattleReport{
|
||||
BattleReportWave: []*pb.BigHuntBattleReportWave{},
|
||||
},
|
||||
DiffUserData: buildBigHuntDiff(user, append([]string{
|
||||
"IUserQuest",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
}, bigHuntDiffTables...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) RestartBigHuntQuest(ctx context.Context, req *pb.RestartBigHuntQuestRequest) (*pb.RestartBigHuntQuestResponse, error) {
|
||||
log.Printf("[BigHuntService] RestartBigHuntQuest: bossQuestId=%d questId=%d", req.BigHuntBossQuestId, req.BigHuntQuestId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
bhQuest := s.catalog.QuestById[req.BigHuntQuestId]
|
||||
|
||||
var battleBinary []byte
|
||||
var deckNumber int32
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleBigHuntQuestStart(user, bhQuest.QuestId, user.BigHuntDeckNumber, nowMillis)
|
||||
|
||||
user.BigHuntProgress.CurrentQuestSceneId = 0
|
||||
user.BigHuntProgress.LatestVersion = nowMillis
|
||||
|
||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||
st.DailyChallengeCount++
|
||||
st.LatestChallengeDatetime = nowMillis
|
||||
st.LatestVersion = nowMillis
|
||||
user.BigHuntStatuses[req.BigHuntBossQuestId] = st
|
||||
|
||||
battleBinary = user.BigHuntBattleBinary
|
||||
deckNumber = user.BigHuntDeckNumber
|
||||
})
|
||||
|
||||
return &pb.RestartBigHuntQuestResponse{
|
||||
BattleBinary: battleBinary,
|
||||
DeckNumber: deckNumber,
|
||||
DiffUserData: buildBigHuntDiff(user, append([]string{"IUserQuest"}, bigHuntDiffTables...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) SkipBigHuntQuest(ctx context.Context, req *pb.SkipBigHuntQuestRequest) (*pb.SkipBigHuntQuestResponse, error) {
|
||||
log.Printf("[BigHuntService] SkipBigHuntQuest: bossQuestId=%d skipCount=%d", req.BigHuntBossQuestId, req.SkipCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
st := user.BigHuntStatuses[req.BigHuntBossQuestId]
|
||||
st.DailyChallengeCount += req.SkipCount
|
||||
st.LatestChallengeDatetime = nowMillis
|
||||
st.LatestVersion = nowMillis
|
||||
user.BigHuntStatuses[req.BigHuntBossQuestId] = st
|
||||
})
|
||||
|
||||
return &pb.SkipBigHuntQuestResponse{
|
||||
ScoreReward: []*pb.BigHuntReward{},
|
||||
DiffUserData: buildBigHuntDiff(user, bigHuntDiffTables),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) SaveBigHuntBattleInfo(ctx context.Context, req *pb.SaveBigHuntBattleInfoRequest) (*pb.SaveBigHuntBattleInfoResponse, error) {
|
||||
log.Printf("[BigHuntService] SaveBigHuntBattleInfo: elapsedFrames=%d", req.ElapsedFrameCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
var totalDamage int64
|
||||
if req.BigHuntBattleDetail != nil {
|
||||
for _, ci := range req.BigHuntBattleDetail.CostumeBattleInfo {
|
||||
if ci != nil {
|
||||
totalDamage += ci.TotalDamage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.BigHuntBattleBinary = req.BattleBinary
|
||||
|
||||
if req.BigHuntBattleDetail != nil {
|
||||
user.BigHuntBattleDetail = store.BigHuntBattleDetail{
|
||||
DeckType: req.BigHuntBattleDetail.DeckType,
|
||||
UserTripleDeckNumber: req.BigHuntBattleDetail.UserTripleDeckNumber,
|
||||
BossKnockDownCount: req.BigHuntBattleDetail.BossKnockDownCount,
|
||||
MaxComboCount: req.BigHuntBattleDetail.MaxComboCount,
|
||||
TotalDamage: totalDamage,
|
||||
}
|
||||
}
|
||||
|
||||
user.BigHuntProgress.LatestVersion = nowMillis
|
||||
})
|
||||
|
||||
return &pb.SaveBigHuntBattleInfoResponse{
|
||||
DiffUserData: buildBigHuntDiff(user, []string{"IUserBigHuntProgressStatus"}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb.Empty) (*pb.GetBigHuntTopDataResponse, error) {
|
||||
log.Printf("[BigHuntService] GetBigHuntTopData")
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.SnapshotUser(userId)
|
||||
|
||||
nowMillis := gametime.NowMillis()
|
||||
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
||||
|
||||
var weeklyScoreResults []*pb.WeeklyScoreResult
|
||||
for _, boss := range s.catalog.BossByBossId {
|
||||
key := store.BigHuntWeeklyScoreKey{
|
||||
BigHuntWeeklyVersion: weeklyVersion,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
ws := user.BigHuntWeeklyMaxScores[key]
|
||||
gradeIconId := s.catalog.ResolveGradeIconId(boss.BigHuntBossId, ws.MaxScore)
|
||||
|
||||
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
||||
AttributeType: boss.AttributeType,
|
||||
BeforeMaxScore: ws.MaxScore,
|
||||
CurrentMaxScore: ws.MaxScore,
|
||||
BeforeAssetGradeIconId: gradeIconId,
|
||||
CurrentAssetGradeIconId: gradeIconId,
|
||||
AfterMaxScore: ws.MaxScore,
|
||||
AfterAssetGradeIconId: gradeIconId,
|
||||
})
|
||||
}
|
||||
|
||||
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
||||
|
||||
weeklyRewards := s.resolveWeeklyRewards(user, weeklyVersion, nowMillis)
|
||||
|
||||
lastWeekVersion := weeklyVersion - 7*24*60*60*1000
|
||||
lastWeekRewards := s.resolveWeeklyRewards(user, lastWeekVersion, nowMillis)
|
||||
|
||||
return &pb.GetBigHuntTopDataResponse{
|
||||
WeeklyScoreResult: weeklyScoreResults,
|
||||
WeeklyScoreReward: weeklyRewards,
|
||||
IsReceivedWeeklyScoreReward: ws.IsReceivedWeeklyReward,
|
||||
LastWeekWeeklyScoreReward: lastWeekRewards,
|
||||
DiffUserData: buildBigHuntDiff(user, bigHuntDiffTables),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BigHuntServiceServer) resolveWeeklyRewards(user store.UserState, weeklyVersion, nowMillis int64) []*pb.BigHuntReward {
|
||||
var rewards []*pb.BigHuntReward
|
||||
for _, boss := range s.catalog.BossByBossId {
|
||||
rewardKey := masterdata.BigHuntWeeklyRewardKey{
|
||||
ScheduleId: 1,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
rewardGroupId := s.catalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
|
||||
if rewardGroupId == 0 {
|
||||
continue
|
||||
}
|
||||
weekKey := store.BigHuntWeeklyScoreKey{
|
||||
BigHuntWeeklyVersion: weeklyVersion,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
||||
for _, item := range s.catalog.CollectNewRewards(rewardGroupId, 0, maxScore) {
|
||||
rewards = append(rewards, &pb.BigHuntReward{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
if rewards == nil {
|
||||
rewards = []*pb.BigHuntReward{}
|
||||
}
|
||||
return rewards
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
|
||||
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||
})
|
||||
|
||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||
for i, d := range drops {
|
||||
pbDrops[i] = &pb.BattleDropReward{
|
||||
QuestSceneId: d.QuestSceneId,
|
||||
BattleDropCategoryId: d.BattleDropCategoryId,
|
||||
BattleDropEffectId: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.StartEventQuestResponse{
|
||||
BattleDropReward: pbDrops,
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserStatus",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserEventQuestProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.FinishEventQuestRequest) (*pb.FinishEventQuestResponse, error) {
|
||||
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
|
||||
|
||||
nowMillis := gametime.NowMillis()
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
var outcome questflow.FinishOutcome
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
outcome = s.engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.FinishEventQuestResponse{
|
||||
DropReward: toProtoRewards(outcome.DropRewards),
|
||||
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
||||
MissionClearReward: toProtoRewards(outcome.MissionClearRewards),
|
||||
MissionClearCompleteReward: toProtoRewards(outcome.MissionClearCompleteRewards),
|
||||
AutoOrbitResult: []*pb.QuestReward{},
|
||||
IsBigWin: outcome.IsBigWin,
|
||||
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserEventQuestProgressStatus",
|
||||
"IUserStatus",
|
||||
"IUserGem",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) RestartEventQuest(ctx context.Context, req *pb.RestartEventQuestRequest) (*pb.RestartEventQuestResponse, error) {
|
||||
log.Printf("[QuestService] RestartEventQuest: chapterId=%d questId=%d", req.EventQuestChapterId, req.QuestId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleEventQuestRestart(user, req.EventQuestChapterId, req.QuestId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
return &pb.RestartEventQuestResponse{
|
||||
BattleDropReward: []*pb.BattleDropReward{},
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserEventQuestProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) UpdateEventQuestSceneProgress(ctx context.Context, req *pb.UpdateEventQuestSceneProgressRequest) (*pb.UpdateEventQuestSceneProgressResponse, error) {
|
||||
log.Printf("[QuestService] UpdateEventQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleEventQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
return &pb.UpdateEventQuestSceneProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserEventQuestProgressStatus",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const defaultGuerrillaFreeOpenMinutes = int32(60)
|
||||
|
||||
func (s *QuestServiceServer) StartGuerrillaFreeOpen(ctx context.Context, req *emptypb.Empty) (*pb.StartGuerrillaFreeOpenResponse, error) {
|
||||
log.Printf("[QuestService] StartGuerrillaFreeOpen")
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.GuerrillaFreeOpen.StartDatetime = nowMillis
|
||||
user.GuerrillaFreeOpen.OpenMinutes = defaultGuerrillaFreeOpenMinutes
|
||||
user.GuerrillaFreeOpen.DailyOpenedCount++
|
||||
user.GuerrillaFreeOpen.LatestVersion = nowMillis
|
||||
})
|
||||
|
||||
return &pb.StartGuerrillaFreeOpenResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{"IUserEventQuestGuerrillaFreeOpen"}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (s *QuestServiceServer) StartExtraQuest(ctx context.Context, req *pb.StartExtraQuestRequest) (*pb.StartExtraQuestResponse, error) {
|
||||
log.Printf("[QuestService] StartExtraQuest: questId=%d deckNumber=%d", req.QuestId, req.UserDeckNumber)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleExtraQuestStart(user, req.QuestId, req.UserDeckNumber, nowMillis)
|
||||
})
|
||||
|
||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||
for i, d := range drops {
|
||||
pbDrops[i] = &pb.BattleDropReward{
|
||||
QuestSceneId: d.QuestSceneId,
|
||||
BattleDropCategoryId: d.BattleDropCategoryId,
|
||||
BattleDropEffectId: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.StartExtraQuestResponse{
|
||||
BattleDropReward: pbDrops,
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserStatus",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserExtraQuestProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) FinishExtraQuest(ctx context.Context, req *pb.FinishExtraQuestRequest) (*pb.FinishExtraQuestResponse, error) {
|
||||
log.Printf("[QuestService] FinishExtraQuest: questId=%d isRetired=%v isAnnihilated=%v", req.QuestId, req.IsRetired, req.IsAnnihilated)
|
||||
|
||||
nowMillis := gametime.NowMillis()
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
var outcome questflow.FinishOutcome
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
outcome = s.engine.HandleExtraQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.FinishExtraQuestResponse{
|
||||
DropReward: toProtoRewards(outcome.DropRewards),
|
||||
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
||||
MissionClearReward: toProtoRewards(outcome.MissionClearRewards),
|
||||
MissionClearCompleteReward: toProtoRewards(outcome.MissionClearCompleteRewards),
|
||||
IsBigWin: outcome.IsBigWin,
|
||||
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserExtraQuestProgressStatus",
|
||||
"IUserStatus",
|
||||
"IUserGem",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) RestartExtraQuest(ctx context.Context, req *pb.RestartExtraQuestRequest) (*pb.RestartExtraQuestResponse, error) {
|
||||
log.Printf("[QuestService] RestartExtraQuest: questId=%d", req.QuestId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleExtraQuestRestart(user, req.QuestId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||
for i, d := range drops {
|
||||
pbDrops[i] = &pb.BattleDropReward{
|
||||
QuestSceneId: d.QuestSceneId,
|
||||
BattleDropCategoryId: d.BattleDropCategoryId,
|
||||
BattleDropEffectId: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.RestartExtraQuestResponse{
|
||||
BattleDropReward: pbDrops,
|
||||
DeckNumber: user.Quests[req.QuestId].UserDeckNumber,
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserExtraQuestProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) UpdateExtraQuestSceneProgress(ctx context.Context, req *pb.UpdateExtraQuestSceneProgressRequest) (*pb.UpdateExtraQuestSceneProgressResponse, error) {
|
||||
log.Printf("[QuestService] UpdateExtraQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleExtraQuestSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
return &pb.UpdateExtraQuestSceneProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserExtraQuestProgressStatus",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type QuestServiceServer struct {
|
||||
pb.UnimplementedQuestServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
engine *questflow.QuestHandler
|
||||
}
|
||||
|
||||
func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *QuestServiceServer {
|
||||
if engine == nil {
|
||||
panic("quest handler is required")
|
||||
}
|
||||
return &QuestServiceServer{users: users, sessions: sessions, engine: engine}
|
||||
}
|
||||
|
||||
func buildSelectedQuestDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
|
||||
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) UpdateMainFlowSceneProgress(ctx context.Context, req *pb.UpdateMainFlowSceneProgressRequest) (*pb.UpdateMainFlowSceneProgressResponse, error) {
|
||||
log.Printf("[QuestService] UpdateMainFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleMainFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
return &pb.UpdateMainFlowSceneProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserPortalCageStatus",
|
||||
"IUserSideStoryQuestSceneProgressStatus",
|
||||
"IUserQuest",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) UpdateReplayFlowSceneProgress(ctx context.Context, req *pb.UpdateReplayFlowSceneProgressRequest) (*pb.UpdateReplayFlowSceneProgressResponse, error) {
|
||||
log.Printf("[QuestService] UpdateReplayFlowSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleReplayFlowSceneProgress(user, req.QuestSceneId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
return &pb.UpdateReplayFlowSceneProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestReplayFlowStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, req *pb.UpdateMainQuestSceneProgressRequest) (*pb.UpdateMainQuestSceneProgressResponse, error) {
|
||||
log.Printf("[QuestService] UpdateMainQuestSceneProgress: questSceneId=%d", req.QuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleMainQuestSceneProgress(user, req.QuestSceneId)
|
||||
})
|
||||
|
||||
return &pb.UpdateMainQuestSceneProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserStatus",
|
||||
"IUserCharacter",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
|
||||
log.Printf("[QuestService] StartMainQuest: %+v", req)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if req.IsReplayFlow {
|
||||
s.engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||
} else {
|
||||
s.engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||
}
|
||||
})
|
||||
|
||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||
for i, d := range drops {
|
||||
pbDrops[i] = &pb.BattleDropReward{
|
||||
QuestSceneId: d.QuestSceneId,
|
||||
BattleDropCategoryId: d.BattleDropCategoryId,
|
||||
BattleDropEffectId: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.StartMainQuestResponse{
|
||||
BattleDropReward: pbDrops,
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserStatus",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserMainQuestReplayFlowStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
|
||||
if len(grants) == 0 {
|
||||
return []*pb.QuestReward{}
|
||||
}
|
||||
out := make([]*pb.QuestReward, len(grants))
|
||||
for i, g := range grants {
|
||||
out[i] = &pb.QuestReward{
|
||||
PossessionType: int32(g.PossessionType),
|
||||
PossessionId: g.PossessionId,
|
||||
Count: g.Count,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.FinishMainQuestRequest) (*pb.FinishMainQuestResponse, error) {
|
||||
log.Printf("[QuestService] FinishMainQuest: questId=%d isMainFlow=%v isRetired=%v isAnnihilated=%v storySkipType=%d",
|
||||
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
|
||||
|
||||
nowMillis := gametime.NowMillis()
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
var outcome questflow.FinishOutcome
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
outcome = s.engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.FinishMainQuestResponse{
|
||||
DropReward: toProtoRewards(outcome.DropRewards),
|
||||
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
||||
MissionClearReward: toProtoRewards(outcome.MissionClearRewards),
|
||||
MissionClearCompleteReward: toProtoRewards(outcome.MissionClearCompleteRewards),
|
||||
AutoOrbitResult: []*pb.QuestReward{},
|
||||
IsBigWin: outcome.IsBigWin,
|
||||
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
||||
ReplayFlowFirstClearReward: toProtoRewards(outcome.ReplayFlowFirstClearRewards),
|
||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserMainQuestReplayFlowStatus",
|
||||
"IUserStatus",
|
||||
"IUserGem",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
"IUserCompanion",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.RestartMainQuestRequest) (*pb.RestartMainQuestResponse, error) {
|
||||
log.Printf("[QuestService] RestartMainQuest: questId=%d isMainFlow=%v", req.QuestId, req.IsMainFlow)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
s.engine.HandleQuestRestart(user, req.QuestId, gametime.NowMillis())
|
||||
})
|
||||
|
||||
drops := s.engine.BattleDropRewards(req.QuestId)
|
||||
pbDrops := make([]*pb.BattleDropReward, len(drops))
|
||||
for i, d := range drops {
|
||||
pbDrops[i] = &pb.BattleDropReward{
|
||||
QuestSceneId: d.QuestSceneId,
|
||||
BattleDropCategoryId: d.BattleDropCategoryId,
|
||||
BattleDropEffectId: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.RestartMainQuestResponse{
|
||||
BattleDropReward: pbDrops,
|
||||
DeckNumber: user.Quests[req.QuestId].UserDeckNumber,
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserStatus",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) FinishAutoOrbit(ctx context.Context, req *emptypb.Empty) (*pb.FinishAutoOrbitResponse, error) {
|
||||
log.Printf("[QuestService] FinishAutoOrbit")
|
||||
return &pb.FinishAutoOrbitResponse{
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestRequest) (*pb.SkipQuestResponse, error) {
|
||||
log.Printf("[QuestService] SkipQuest: questId=%d skipCount=%d useEffectItems=%d", req.QuestId, req.SkipCount, len(req.UseEffectItem))
|
||||
|
||||
nowMillis := gametime.NowMillis()
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
var outcome questflow.FinishOutcome
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, item := range req.UseEffectItem {
|
||||
log.Printf("[QuestService] SkipQuest UseEffectItem: consumableItemId=%d count=%d", item.ConsumableItemId, item.Count)
|
||||
user.ConsumableItems[item.ConsumableItemId] -= item.Count
|
||||
if user.ConsumableItems[item.ConsumableItemId] < 0 {
|
||||
user.ConsumableItems[item.ConsumableItemId] = 0
|
||||
}
|
||||
}
|
||||
outcome = s.engine.HandleQuestSkip(user, req.QuestId, req.SkipCount, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.SkipQuestResponse{
|
||||
DropReward: toProtoRewards(outcome.DropRewards),
|
||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserQuest",
|
||||
"IUserStatus",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserParts",
|
||||
"IUserPartsGroupNote",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteRequest) (*pb.SetRouteResponse, error) {
|
||||
log.Printf("[QuestService] SetRoute: mainQuestRouteId=%d", req.MainQuestRouteId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
||||
if seasonId, ok := s.engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
||||
user.MainQuest.MainQuestSeasonId = seasonId
|
||||
}
|
||||
now := gametime.NowMillis()
|
||||
user.PortalCageStatus.IsCurrentProgress = false
|
||||
user.PortalCageStatus.LatestVersion = now
|
||||
})
|
||||
|
||||
return &pb.SetRouteResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserPortalCageStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) SetQuestSceneChoice(ctx context.Context, req *pb.SetQuestSceneChoiceRequest) (*pb.SetQuestSceneChoiceResponse, error) {
|
||||
log.Printf("[QuestService] SetQuestSceneChoice: questSceneId=%d choiceNumber=%d",
|
||||
req.QuestSceneId, req.ChoiceNumber)
|
||||
return &pb.SetQuestSceneChoiceResponse{
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) ResetLimitContentQuestProgress(ctx context.Context, req *pb.ResetLimitContentQuestProgressRequest) (*pb.ResetLimitContentQuestProgressResponse, error) {
|
||||
log.Printf("[QuestService] ResetLimitContentQuestProgress: eventQuestChapterId=%d questId=%d",
|
||||
req.EventQuestChapterId, req.QuestId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if _, exists := user.SideStoryQuests[req.QuestId]; exists {
|
||||
user.SideStoryQuests[req.QuestId] = store.SideStoryQuestProgress{
|
||||
HeadSideStoryQuestSceneId: 0,
|
||||
SideStoryQuestStateType: model.SideStoryQuestStateUnknown,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
delete(user.QuestLimitContentStatus, req.QuestId)
|
||||
|
||||
if user.SideStoryActiveProgress.CurrentSideStoryQuestId == req.QuestId {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return &pb.ResetLimitContentQuestProgressResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserSideStoryQuest",
|
||||
"IUserSideStoryQuestSceneProgressStatus",
|
||||
"IUserQuestLimitContentStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *QuestServiceServer) SetAutoSaleSetting(ctx context.Context, req *pb.SetAutoSaleSettingRequest) (*pb.SetAutoSaleSettingResponse, error) {
|
||||
log.Printf("[QuestService] SetAutoSaleSetting: items=%d", len(req.AutoSaleSettingItem))
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState, len(req.AutoSaleSettingItem))
|
||||
for itemType, itemValue := range req.AutoSaleSettingItem {
|
||||
user.AutoSaleSettings[itemType] = store.AutoSaleSettingState{
|
||||
PossessionAutoSaleItemType: itemType,
|
||||
PossessionAutoSaleItemValue: itemValue,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return &pb.SetAutoSaleSettingResponse{
|
||||
DiffUserData: buildSelectedQuestDiff(user, []string{
|
||||
"IUserAutoSaleSettingDetail",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type SideStoryQuestServiceServer struct {
|
||||
pb.UnimplementedSideStoryQuestServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.SideStoryCatalog
|
||||
}
|
||||
|
||||
func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.SideStoryCatalog) *SideStoryQuestServiceServer {
|
||||
return &SideStoryQuestServiceServer{users: users, sessions: sessions, catalog: catalog}
|
||||
}
|
||||
|
||||
func buildSideStoryDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
|
||||
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
|
||||
}
|
||||
|
||||
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
|
||||
log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
firstSceneId := s.catalog.FirstSceneByQuestId[req.SideStoryQuestId]
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
|
||||
|
||||
var sceneId int32
|
||||
if exists && existing.HeadSideStoryQuestSceneId > 0 {
|
||||
sceneId = existing.HeadSideStoryQuestSceneId
|
||||
} else {
|
||||
sceneId = firstSceneId
|
||||
}
|
||||
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestId = req.SideStoryQuestId
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = sceneId
|
||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
||||
|
||||
if !exists {
|
||||
user.SideStoryQuests[req.SideStoryQuestId] = store.SideStoryQuestProgress{
|
||||
HeadSideStoryQuestSceneId: firstSceneId,
|
||||
SideStoryQuestStateType: model.SideStoryQuestStateActive,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return &pb.MoveSideStoryQuestResponse{
|
||||
DiffUserData: buildSideStoryDiff(user, []string{
|
||||
"IUserSideStoryQuest",
|
||||
"IUserSideStoryQuestSceneProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx context.Context, req *pb.UpdateSideStoryQuestSceneProgressRequest) (*pb.UpdateSideStoryQuestSceneProgressResponse, error) {
|
||||
log.Printf("[SideStoryQuestService] UpdateSideStoryQuestSceneProgress: sideStoryQuestId=%d sceneId=%d",
|
||||
req.SideStoryQuestId, req.SideStoryQuestSceneId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
||||
|
||||
progress := user.SideStoryQuests[req.SideStoryQuestId]
|
||||
if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId {
|
||||
progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
||||
}
|
||||
progress.LatestVersion = nowMillis
|
||||
user.SideStoryQuests[req.SideStoryQuestId] = progress
|
||||
})
|
||||
|
||||
return &pb.UpdateSideStoryQuestSceneProgressResponse{
|
||||
DiffUserData: buildSideStoryDiff(user, []string{
|
||||
"IUserSideStoryQuest",
|
||||
"IUserSideStoryQuestSceneProgressStatus",
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type RewardServiceServer struct {
|
||||
pb.UnimplementedRewardServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
bhCatalog *masterdata.BigHuntCatalog
|
||||
granter *store.PossessionGranter
|
||||
}
|
||||
|
||||
func NewRewardServiceServer(
|
||||
users store.UserRepository,
|
||||
sessions store.SessionRepository,
|
||||
bhCatalog *masterdata.BigHuntCatalog,
|
||||
granter *store.PossessionGranter,
|
||||
) *RewardServiceServer {
|
||||
return &RewardServiceServer{users: users, sessions: sessions, bhCatalog: bhCatalog, granter: granter}
|
||||
}
|
||||
|
||||
func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveBigHuntRewardResponse, error) {
|
||||
log.Printf("[RewardService] ReceiveBigHuntReward")
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
weeklyVersion := gametime.WeeklyVersion(nowMillis)
|
||||
|
||||
var weeklyScoreResults []*pb.WeeklyScoreResult
|
||||
var weeklyRewards []*pb.BigHuntReward
|
||||
isReceived := false
|
||||
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
ws := user.BigHuntWeeklyStatuses[weeklyVersion]
|
||||
isReceived = ws.IsReceivedWeeklyReward
|
||||
|
||||
for _, boss := range s.bhCatalog.BossByBossId {
|
||||
key := store.BigHuntWeeklyScoreKey{
|
||||
BigHuntWeeklyVersion: weeklyVersion,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
wms := user.BigHuntWeeklyMaxScores[key]
|
||||
gradeIcon := s.bhCatalog.ResolveGradeIconId(boss.BigHuntBossId, wms.MaxScore)
|
||||
weeklyScoreResults = append(weeklyScoreResults, &pb.WeeklyScoreResult{
|
||||
AttributeType: boss.AttributeType,
|
||||
BeforeMaxScore: wms.MaxScore,
|
||||
CurrentMaxScore: wms.MaxScore,
|
||||
BeforeAssetGradeIconId: gradeIcon,
|
||||
CurrentAssetGradeIconId: gradeIcon,
|
||||
AfterMaxScore: wms.MaxScore,
|
||||
AfterAssetGradeIconId: gradeIcon,
|
||||
})
|
||||
}
|
||||
|
||||
if !isReceived {
|
||||
for _, boss := range s.bhCatalog.BossByBossId {
|
||||
rewardKey := masterdata.BigHuntWeeklyRewardKey{
|
||||
ScheduleId: 1,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
rewardGroupId := s.bhCatalog.ResolveActiveWeeklyRewardGroupId(rewardKey, nowMillis)
|
||||
if rewardGroupId == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
weekKey := store.BigHuntWeeklyScoreKey{
|
||||
BigHuntWeeklyVersion: weeklyVersion,
|
||||
AttributeType: boss.AttributeType,
|
||||
}
|
||||
maxScore := user.BigHuntWeeklyMaxScores[weekKey].MaxScore
|
||||
|
||||
items := s.bhCatalog.CollectNewRewards(rewardGroupId, 0, maxScore)
|
||||
for _, item := range items {
|
||||
s.granter.GrantFull(user, model.PossessionType(item.PossessionType), item.PossessionId, item.Count, nowMillis)
|
||||
weeklyRewards = append(weeklyRewards, &pb.BigHuntReward{
|
||||
PossessionType: item.PossessionType,
|
||||
PossessionId: item.PossessionId,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ws.IsReceivedWeeklyReward = true
|
||||
ws.LatestVersion = nowMillis
|
||||
user.BigHuntWeeklyStatuses[weeklyVersion] = ws
|
||||
isReceived = true
|
||||
}
|
||||
})
|
||||
|
||||
if weeklyRewards == nil {
|
||||
weeklyRewards = []*pb.BigHuntReward{}
|
||||
}
|
||||
if weeklyScoreResults == nil {
|
||||
weeklyScoreResults = []*pb.WeeklyScoreResult{}
|
||||
}
|
||||
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserBigHuntWeeklyStatus",
|
||||
"IUserBigHuntWeeklyMaxScore",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
})
|
||||
|
||||
return &pb.ReceiveBigHuntRewardResponse{
|
||||
WeeklyScoreResult: weeklyScoreResults,
|
||||
WeeklyScoreReward: weeklyRewards,
|
||||
IsReceivedWeeklyScoreReward: isReceived,
|
||||
LastWeekWeeklyScoreReward: []*pb.BigHuntReward{},
|
||||
DiffUserData: userdata.BuildDiffFromTables(tables),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RewardServiceServer) ReceivePvpReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceivePvpRewardResponse, error) {
|
||||
log.Printf("[RewardService] ReceivePvpReward (stub)")
|
||||
return &pb.ReceivePvpRewardResponse{
|
||||
DiffUserData: map[string]*pb.DiffData{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RewardServiceServer) ReceiveLabyrinthSeasonReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveLabyrinthSeasonRewardResponse, error) {
|
||||
log.Printf("[RewardService] ReceiveLabyrinthSeasonReward (stub)")
|
||||
return &pb.ReceiveLabyrinthSeasonRewardResponse{
|
||||
DiffUserData: map[string]*pb.DiffData{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RewardServiceServer) ReceiveMissionPassRemainingReward(ctx context.Context, _ *emptypb.Empty) (*pb.ReceiveMissionPassRemainingRewardResponse, error) {
|
||||
log.Printf("[RewardService] ReceiveMissionPassRemainingReward (stub)")
|
||||
return &pb.ReceiveMissionPassRemainingRewardResponse{
|
||||
DiffUserData: map[string]*pb.DiffData{},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var shopDiffTables = []string{
|
||||
"IUserShopItem",
|
||||
"IUserShopReplaceable",
|
||||
"IUserShopReplaceableLineup",
|
||||
"IUserGem",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserPremiumItem",
|
||||
"IUserStatus",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserCharacter",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
}
|
||||
|
||||
type ShopServiceServer struct {
|
||||
pb.UnimplementedShopServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.ShopCatalog
|
||||
granter *store.PossessionGranter
|
||||
}
|
||||
|
||||
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer {
|
||||
return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) {
|
||||
log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for shopItemId, qty := range req.ShopItems {
|
||||
item, ok := s.catalog.Items[shopItemId]
|
||||
if !ok {
|
||||
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
|
||||
continue
|
||||
}
|
||||
|
||||
totalPrice := item.Price * qty
|
||||
if err := store.DeductPrice(user, item.PriceType, item.PriceId, totalPrice); err != nil {
|
||||
log.Printf("[ShopService] Buy: deduct failed shopItemId=%d: %v", shopItemId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, content := range s.catalog.Contents[shopItemId] {
|
||||
s.granter.GrantFull(user,
|
||||
model.PossessionType(content.PossessionType),
|
||||
content.PossessionId,
|
||||
content.Count*qty,
|
||||
nowMillis,
|
||||
)
|
||||
}
|
||||
|
||||
s.applyContentEffects(user, shopItemId, qty, nowMillis)
|
||||
|
||||
si := user.ShopItems[shopItemId]
|
||||
si.ShopItemId = shopItemId
|
||||
si.BoughtCount += qty
|
||||
si.LatestBoughtCountChangedDatetime = nowMillis
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[shopItemId] = si
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shop buy: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.BuyResponse{
|
||||
OverflowPossession: []*pb.Possession{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
||||
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 {
|
||||
for i, itemId := range s.catalog.ItemShopPool {
|
||||
slot := int32(i + 1)
|
||||
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
|
||||
SlotNumber: slot,
|
||||
ShopItemId: itemId,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.IsGemUsed {
|
||||
user.ShopReplaceable.LineupUpdateCount++
|
||||
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
|
||||
for _, itemId := range s.catalog.ItemShopPool {
|
||||
if si, ok := user.ShopItems[itemId]; ok {
|
||||
si.BoughtCount = 0
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[itemId] = si
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shop refresh: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.RefreshResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) GetCesaLimit(_ context.Context, _ *emptypb.Empty) (*pb.GetCesaLimitResponse, error) {
|
||||
log.Printf("[ShopService] GetCesaLimit")
|
||||
return &pb.GetCesaLimitResponse{
|
||||
CesaLimit: []*pb.CesaLimit{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *pb.CreatePurchaseTransactionRequest) (*pb.CreatePurchaseTransactionResponse, error) {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
|
||||
req.ShopId, req.ShopItemId, req.ProductId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
item, ok := s.catalog.Items[req.ShopItemId]
|
||||
if !ok {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeductPrice(user, item.PriceType, item.PriceId, item.Price); err != nil {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
|
||||
}
|
||||
|
||||
for _, content := range s.catalog.Contents[req.ShopItemId] {
|
||||
s.granter.GrantFull(user,
|
||||
model.PossessionType(content.PossessionType),
|
||||
content.PossessionId,
|
||||
content.Count,
|
||||
nowMillis,
|
||||
)
|
||||
}
|
||||
|
||||
s.applyContentEffects(user, req.ShopItemId, 1, nowMillis)
|
||||
|
||||
si := user.ShopItems[req.ShopItemId]
|
||||
si.ShopItemId = req.ShopItemId
|
||||
si.BoughtCount++
|
||||
if item.ShopItemLimitedStockId > 0 {
|
||||
if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
|
||||
si.BoughtCount = 0
|
||||
}
|
||||
}
|
||||
si.LatestBoughtCountChangedDatetime = nowMillis
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[req.ShopItemId] = si
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create purchase transaction: %w", err)
|
||||
}
|
||||
|
||||
txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis)
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.CreatePurchaseTransactionResponse{
|
||||
PurchaseTransactionId: txId,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context, req *pb.PurchaseGooglePlayStoreProductRequest) (*pb.PurchaseGooglePlayStoreProductResponse, error) {
|
||||
log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
snapshot, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("purchase google play: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.PurchaseGooglePlayStoreProductResponse{
|
||||
OverflowPossession: []*pb.Possession{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) {
|
||||
for _, effect := range s.catalog.Effects[shopItemId] {
|
||||
switch effect.EffectTargetType {
|
||||
case model.EffectTargetStaminaRecovery:
|
||||
maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level]
|
||||
millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level)
|
||||
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
|
||||
default:
|
||||
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 {
|
||||
switch effectValueType {
|
||||
case model.EffectValueFixed:
|
||||
return effectValue
|
||||
case model.EffectValuePermil:
|
||||
maxMillis := s.catalog.MaxStaminaMillis[userLevel]
|
||||
return effectValue * maxMillis / 1000
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"lunar-tear/server/internal/store"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var startedGameStartTables = []string{
|
||||
"IUserProfile",
|
||||
"IUserCharacter",
|
||||
"IUserCostume",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserCompanion",
|
||||
"IUserDeckCharacter",
|
||||
"IUserDeck",
|
||||
"IUserGem",
|
||||
"IUserMission",
|
||||
"IUserMainQuestFlowStatus",
|
||||
"IUserMainQuestMainFlowStatus",
|
||||
"IUserMainQuestProgressStatus",
|
||||
"IUserMainQuestSeasonRoute",
|
||||
"IUserQuest",
|
||||
"IUserQuestMission",
|
||||
"IUserTutorialProgress",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserDeckTypeNote",
|
||||
"IUserDeckSubWeaponGroup",
|
||||
"IUserDeckPartsGroup",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
}
|
||||
|
||||
var gimmickDiffTables = []string{
|
||||
"IUserGimmick",
|
||||
"IUserGimmickOrnamentProgress",
|
||||
"IUserGimmickSequence",
|
||||
"IUserGimmickUnlock",
|
||||
}
|
||||
|
||||
func currentUserId(ctx context.Context, users store.UserRepository, sessions store.SessionRepository) int64 {
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if vals := md.Get("x-session-key"); len(vals) > 0 {
|
||||
if userId, err := sessions.ResolveUserId(vals[0]); err == nil {
|
||||
return userId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultId, _ := users.DefaultUserId()
|
||||
return defaultId
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
type TutorialServiceServer struct {
|
||||
pb.UnimplementedTutorialServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
engine *questflow.QuestHandler
|
||||
}
|
||||
|
||||
func NewTutorialServiceServer(users store.UserRepository, sessions store.SessionRepository, engine *questflow.QuestHandler) *TutorialServiceServer {
|
||||
return &TutorialServiceServer{users: users, sessions: sessions, engine: engine}
|
||||
}
|
||||
|
||||
func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb.SetTutorialProgressRequest) (*pb.SetTutorialProgressResponse, error) {
|
||||
log.Printf("[TutorialService] SetTutorialProgress: type=%d phase=%d choice=%d", req.TutorialType, req.ProgressPhase, req.ChoiceId)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
var grants []questflow.RewardGrant
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Tutorials[req.TutorialType] = store.TutorialProgressState{
|
||||
TutorialType: req.TutorialType,
|
||||
ProgressPhase: req.ProgressPhase,
|
||||
ChoiceId: req.ChoiceId,
|
||||
}
|
||||
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
|
||||
req.TutorialType == int32(model.TutorialTypeMenuSecond) {
|
||||
store.EnsureDefaultDeck(user, nowMillis)
|
||||
}
|
||||
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
|
||||
})
|
||||
tables := []string{"IUserTutorialProgress"}
|
||||
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
|
||||
req.TutorialType == int32(model.TutorialTypeMenuSecond) {
|
||||
tables = append(tables,
|
||||
"IUserCharacter", "IUserCostume", "IUserWeapon",
|
||||
"IUserWeaponSkill", "IUserWeaponAbility",
|
||||
"IUserCompanion", "IUserDeckCharacter", "IUserDeck",
|
||||
)
|
||||
}
|
||||
if len(grants) > 0 {
|
||||
tables = append(tables, "IUserCompanion")
|
||||
}
|
||||
result := userdata.SelectTables(userdata.FullClientTableMap(user), tables)
|
||||
for _, t := range tables {
|
||||
log.Printf("[TutorialService] DiffTable %s -> %s", t, result[t])
|
||||
}
|
||||
rewards := make([]*pb.TutorialChoiceReward, len(grants))
|
||||
for i, g := range grants {
|
||||
rewards[i] = &pb.TutorialChoiceReward{
|
||||
PossessionType: int32(g.PossessionType),
|
||||
PossessionId: g.PossessionId,
|
||||
Count: g.Count,
|
||||
}
|
||||
}
|
||||
return &pb.SetTutorialProgressResponse{
|
||||
TutorialChoiceReward: rewards,
|
||||
DiffUserData: userdata.BuildDiffFromTables(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TutorialServiceServer) SetTutorialProgressAndReplaceDeck(ctx context.Context, req *pb.SetTutorialProgressAndReplaceDeckRequest) (*pb.SetTutorialProgressAndReplaceDeckResponse, error) {
|
||||
log.Printf("[TutorialService] SetTutorialProgressAndReplaceDeck: type=%d phase=%d deckType=%d deckNumber=%d", req.TutorialType, req.ProgressPhase, req.DeckType, req.UserDeckNumber)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Tutorials[req.TutorialType] = store.TutorialProgressState{
|
||||
TutorialType: req.TutorialType,
|
||||
ProgressPhase: req.ProgressPhase,
|
||||
}
|
||||
if req.Deck != nil {
|
||||
store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis())
|
||||
}
|
||||
})
|
||||
return &pb.SetTutorialProgressAndReplaceDeckResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
|
||||
"IUserTutorialProgress",
|
||||
"IUserDeck",
|
||||
"IUserDeckCharacter",
|
||||
"IUserDeckSubWeaponGroup",
|
||||
})),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type UserServiceServer struct {
|
||||
pb.UnimplementedUserServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
}
|
||||
|
||||
func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository) *UserServiceServer {
|
||||
return &UserServiceServer{users: users, sessions: sessions}
|
||||
}
|
||||
|
||||
func setCommonResponseTrailers(ctx context.Context, diff map[string]*pb.DiffData, includeUpdateNames bool) {
|
||||
keys := make([]string, 0, len(diff))
|
||||
for key := range diff {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var pairs []string
|
||||
if includeUpdateNames && len(keys) > 0 {
|
||||
pairs = append(pairs, "x-apb-update-user-data-names", keys[0])
|
||||
for _, key := range keys[1:] {
|
||||
pairs[len(pairs)-1] += "," + key
|
||||
}
|
||||
}
|
||||
|
||||
if err := grpc.SetTrailer(ctx, metadata.Pairs(pairs...)); err != nil {
|
||||
log.Printf("[UserService] failed to set trailers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
|
||||
user, err := s.users.EnsureUser(req.Uuid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure user: %w", err)
|
||||
}
|
||||
log.Printf("[UserService] RegisterUser: uuid=%s terminalId=%s -> userId=%d", req.Uuid, req.TerminalId, user.UserId)
|
||||
|
||||
return &pb.RegisterUserResponse{
|
||||
UserId: user.UserId,
|
||||
Signature: fmt.Sprintf("sig_%d_%d", user.UserId, gametime.Now().Unix()),
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.FirstEntranceClientTableMap(user)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) Auth(ctx context.Context, req *pb.AuthUserRequest) (*pb.AuthUserResponse, error) {
|
||||
log.Printf("[UserService] Auth: uuid=%s", req.Uuid)
|
||||
|
||||
user, session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
|
||||
return &pb.AuthUserResponse{
|
||||
SessionKey: session.SessionKey,
|
||||
ExpireDatetime: timestamppb.New(session.ExpireAt),
|
||||
Signature: req.Signature,
|
||||
UserId: user.UserId,
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.FirstEntranceClientTableMap(user)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*pb.GameStartResponse, error) {
|
||||
log.Printf("[UserService] GameStart")
|
||||
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if vals := md.Get("x-session-key"); len(vals) > 0 {
|
||||
log.Printf("[UserService] GameStart session: %s", vals[0])
|
||||
}
|
||||
}
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.GameStartDatetime = gametime.NowMillis()
|
||||
})
|
||||
fullTables := userdata.FullClientTableMap(user)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(fullTables, startedGameStartTables))
|
||||
setCommonResponseTrailers(ctx, diff, true)
|
||||
|
||||
return &pb.GameStartResponse{
|
||||
// Apply only the starter outgame rows we need after title completion.
|
||||
// Keep IUser and other risky core-account rows out of GameStart diff.
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) {
|
||||
log.Printf("[UserService] TransferUser")
|
||||
user, err := s.users.EnsureUser(req.Uuid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ensure user: %w", err)
|
||||
}
|
||||
return &pb.TransferUserResponse{
|
||||
UserId: user.UserId,
|
||||
Signature: "transferred-sig",
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) SetUserName(ctx context.Context, req *pb.SetUserNameRequest) (*pb.SetUserNameResponse, error) {
|
||||
log.Printf("[UserService] SetUserName: %s", req.Name)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
user.Profile.Name = req.Name
|
||||
user.Profile.NameUpdateDatetime = nowMillis
|
||||
})
|
||||
return &pb.SetUserNameResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) SetUserMessage(ctx context.Context, req *pb.SetUserMessageRequest) (*pb.SetUserMessageResponse, error) {
|
||||
log.Printf("[UserService] SetUserMessage: %s", req.Message)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
user.Profile.Message = req.Message
|
||||
user.Profile.MessageUpdateDatetime = nowMillis
|
||||
})
|
||||
return &pb.SetUserMessageResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) SetUserFavoriteCostumeId(ctx context.Context, req *pb.SetUserFavoriteCostumeIdRequest) (*pb.SetUserFavoriteCostumeIdResponse, error) {
|
||||
log.Printf("[UserService] SetUserFavoriteCostumeId: %d", req.FavoriteCostumeId)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
nowMillis := gametime.NowMillis()
|
||||
user.Profile.FavoriteCostumeId = req.FavoriteCostumeId
|
||||
user.Profile.FavoriteCostumeIdUpdateDatetime = nowMillis
|
||||
})
|
||||
return &pb.SetUserFavoriteCostumeIdResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserProfileRequest) (*pb.GetUserProfileResponse, error) {
|
||||
log.Printf("[UserService] GetUserProfile: playerId=%d", req.PlayerId)
|
||||
userId := req.PlayerId
|
||||
if userId == 0 {
|
||||
userId = currentUserId(ctx, s.users, s.sessions)
|
||||
}
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return &pb.GetUserProfileResponse{DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
deckCharacters := []*pb.ProfileDeckCharacter{}
|
||||
if deck, ok := user.Decks[store.DeckKey{DeckType: model.DeckTypeQuest, UserDeckNumber: 1}]; ok && deck.UserDeckCharacterUuid01 != "" {
|
||||
if deckCharacter, ok := user.DeckCharacters[deck.UserDeckCharacterUuid01]; ok {
|
||||
costumeId := int32(0)
|
||||
if costume, ok := user.Costumes[deckCharacter.UserCostumeUuid]; ok {
|
||||
costumeId = costume.CostumeId
|
||||
}
|
||||
mainWeaponId := int32(0)
|
||||
mainWeaponLevel := int32(0)
|
||||
if weapon, ok := user.Weapons[deckCharacter.MainUserWeaponUuid]; ok {
|
||||
mainWeaponId = weapon.WeaponId
|
||||
mainWeaponLevel = weapon.Level
|
||||
}
|
||||
deckCharacters = append(deckCharacters, &pb.ProfileDeckCharacter{
|
||||
CostumeId: costumeId,
|
||||
MainWeaponId: mainWeaponId,
|
||||
MainWeaponLevel: mainWeaponLevel,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.GetUserProfileResponse{
|
||||
Level: user.Status.Level,
|
||||
Name: user.Profile.Name,
|
||||
FavoriteCostumeId: user.Profile.FavoriteCostumeId,
|
||||
Message: user.Profile.Message,
|
||||
IsFriend: false,
|
||||
LatestUsedDeck: &pb.ProfileDeck{
|
||||
Power: 100,
|
||||
DeckCharacter: deckCharacters,
|
||||
},
|
||||
PvpInfo: &pb.ProfilePvpInfo{},
|
||||
GamePlayHistory: &pb.GamePlayHistory{
|
||||
HistoryItem: []*pb.PlayHistoryItem{},
|
||||
HistoryCategoryGraphItem: []*pb.PlayHistoryCategoryGraphItem{},
|
||||
},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) SetBirthYearMonth(ctx context.Context, req *pb.SetBirthYearMonthRequest) (*pb.SetBirthYearMonthResponse, error) {
|
||||
log.Printf("[UserService] SetBirthYearMonth: %d/%d", req.BirthYear, req.BirthMonth)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
_, _ = s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.BirthYear = req.BirthYear
|
||||
user.BirthMonth = req.BirthMonth
|
||||
})
|
||||
return &pb.SetBirthYearMonthResponse{DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Empty) (*pb.GetBirthYearMonthResponse, error) {
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return &pb.GetBirthYearMonthResponse{BirthYear: 2000, BirthMonth: 1, DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
return &pb.GetBirthYearMonthResponse{BirthYear: user.BirthYear, BirthMonth: user.BirthMonth, DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetChargeMoney(ctx context.Context, _ *emptypb.Empty) (*pb.GetChargeMoneyResponse, error) {
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: 0, DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: user.ChargeMoneyThisMonth, DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) SetUserSetting(ctx context.Context, req *pb.SetUserSettingRequest) (*pb.SetUserSettingResponse, error) {
|
||||
log.Printf("[UserService] SetUserSetting: isNotifyPurchaseAlert=%v", req.IsNotifyPurchaseAlert)
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.Setting.IsNotifyPurchaseAlert = req.IsNotifyPurchaseAlert
|
||||
})
|
||||
return &pb.SetUserSettingResponse{
|
||||
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserSetting"})),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetAndroidArgs(ctx context.Context, req *pb.GetAndroidArgsRequest) (*pb.GetAndroidArgsResponse, error) {
|
||||
return &pb.GetAndroidArgsResponse{Nonce: "Mama", ApiKey: "1234567890", DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetBackupToken(ctx context.Context, req *pb.GetBackupTokenRequest) (*pb.GetBackupTokenResponse, error) {
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
user, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return &pb.GetBackupTokenResponse{BackupToken: "mock-backup-token", DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
return &pb.GetBackupTokenResponse{BackupToken: user.BackupToken, DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) CheckTransferSetting(ctx context.Context, _ *emptypb.Empty) (*pb.CheckTransferSettingResponse, error) {
|
||||
return &pb.CheckTransferSettingResponse{DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceServer) GetUserGamePlayNote(ctx context.Context, req *pb.GetUserGamePlayNoteRequest) (*pb.GetUserGamePlayNoteResponse, error) {
|
||||
return &pb.GetUserGamePlayNoteResponse{DiffUserData: userdata.EmptyDiff()}, nil
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/gameutil"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
)
|
||||
|
||||
var weaponDiffTables = []string{
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
"IUserWeaponStory",
|
||||
}
|
||||
|
||||
var limitBreakDiffTables = []string{
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserMaterial",
|
||||
"IUserConsumableItem",
|
||||
"IUserWeaponNote",
|
||||
}
|
||||
|
||||
type WeaponServiceServer struct {
|
||||
pb.UnimplementedWeaponServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.WeaponCatalog
|
||||
config *masterdata.GameConfig
|
||||
}
|
||||
|
||||
func NewWeaponServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.WeaponCatalog, config *masterdata.GameConfig) *WeaponServiceServer {
|
||||
return &WeaponServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectRequest) (*pb.ProtectResponse, error) {
|
||||
log.Printf("[WeaponService] Protect: uuids=%v", req.UserWeaponUuid)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, uuid := range req.UserWeaponUuid {
|
||||
weapon, ok := user.Weapons[uuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Protect: weapon uuid=%s not found", uuid)
|
||||
continue
|
||||
}
|
||||
weapon.IsProtected = true
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[uuid] = weapon
|
||||
}
|
||||
})
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
|
||||
return &pb.ProtectResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRequest) (*pb.UnprotectResponse, error) {
|
||||
log.Printf("[WeaponService] Unprotect: uuids=%v", req.UserWeaponUuid)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for _, uuid := range req.UserWeaponUuid {
|
||||
weapon, ok := user.Weapons[uuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Unprotect: weapon uuid=%s not found", uuid)
|
||||
continue
|
||||
}
|
||||
weapon.IsProtected = false
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[uuid] = weapon
|
||||
}
|
||||
})
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
|
||||
return &pb.UnprotectResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.EnhanceByMaterialRequest) (*pb.EnhanceByMaterialResponse, error) {
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
totalExp := int32(0)
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
mat, ok := s.catalog.Materials[materialId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: material id=%d not found, skipping", materialId)
|
||||
continue
|
||||
}
|
||||
|
||||
cur := user.Materials[materialId]
|
||||
if cur < count {
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: insufficient material id=%d have=%d need=%d", materialId, cur, count)
|
||||
continue
|
||||
}
|
||||
user.Materials[materialId] = cur - count
|
||||
totalMaterialCount += count
|
||||
|
||||
expPerUnit := mat.EffectValue
|
||||
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
||||
expPerUnit = expPerUnit * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += expPerUnit * count
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: gold cost=%d (materials=%d)", goldCost, totalMaterialCount)
|
||||
}
|
||||
|
||||
weapon.Exp += totalExp
|
||||
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||
}
|
||||
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
log.Printf("[WeaponService] EnhanceByMaterial: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon enhance by material: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
|
||||
|
||||
return &pb.EnhanceByMaterialResponse{
|
||||
IsGreatSuccess: false,
|
||||
SurplusEnhanceMaterial: map[int32]int32{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*pb.SellResponse, error) {
|
||||
log.Printf("[WeaponService] Sell: uuids=%v", req.UserWeaponUuid)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
|
||||
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
|
||||
Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
totalGold := int32(0)
|
||||
for _, uuid := range req.UserWeaponUuid {
|
||||
weapon, ok := user.Weapons[uuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Sell: weapon uuid=%s not found, skipping", uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Sell: weapon master id=%d not found, skipping", weapon.WeaponId)
|
||||
continue
|
||||
}
|
||||
|
||||
if sellFunc, ok := s.catalog.SellPriceByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
totalGold += sellFunc.Evaluate(weapon.Level)
|
||||
}
|
||||
|
||||
if medals, ok := s.catalog.MedalsByWeaponId[weapon.WeaponId]; ok {
|
||||
for itemId, count := range medals {
|
||||
user.ConsumableItems[itemId] += count
|
||||
}
|
||||
}
|
||||
|
||||
delete(user.Weapons, uuid)
|
||||
delete(user.WeaponSkills, uuid)
|
||||
delete(user.WeaponAbilities, uuid)
|
||||
}
|
||||
|
||||
if totalGold > 0 {
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
|
||||
log.Printf("[WeaponService] Sell: granted %d gold", totalGold)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon sell: %w", err)
|
||||
}
|
||||
|
||||
sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserConsumableItem"}
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), sellDiffTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.SellResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest) (*pb.EvolveResponse, error) {
|
||||
log.Printf("[WeaponService] Evolve: uuid=%s", req.UserWeaponUuid)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Evolve: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Evolve: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
evolvedId, ok := s.catalog.EvolutionNextWeaponId[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] Evolve: no evolution for weaponId=%d", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
totalMaterialCount := int32(0)
|
||||
mats := s.catalog.EvolutionMaterials[wm.WeaponEvolutionMaterialGroupId]
|
||||
for _, mat := range mats {
|
||||
cur := user.Materials[mat.MaterialId]
|
||||
cost := mat.Count
|
||||
if cur < cost {
|
||||
log.Printf("[WeaponService] Evolve: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
|
||||
cost = cur
|
||||
}
|
||||
user.Materials[mat.MaterialId] = cur - cost
|
||||
totalMaterialCount += cost
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.EvolutionCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[WeaponService] Evolve: gold cost=%d", goldCost)
|
||||
}
|
||||
|
||||
weapon.WeaponId = evolvedId
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
|
||||
evolvedMaster, ok := s.catalog.Weapons[evolvedId]
|
||||
if ok {
|
||||
if slots, ok := s.catalog.AbilitySlots[evolvedMaster.WeaponAbilityGroupId]; ok {
|
||||
abilities := make([]store.WeaponAbilityState, len(slots))
|
||||
for i, slot := range slots {
|
||||
abilities[i] = store.WeaponAbilityState{
|
||||
UserWeaponUuid: req.UserWeaponUuid,
|
||||
SlotNumber: slot,
|
||||
Level: 1,
|
||||
}
|
||||
}
|
||||
user.WeaponAbilities[req.UserWeaponUuid] = abilities
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[WeaponService] Evolve: weaponId %d -> %d", wm.WeaponId, evolvedId)
|
||||
|
||||
s.checkEvolutionStoryUnlocks(user, evolvedId, nowMillis)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon evolve: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
|
||||
|
||||
return &pb.EvolveResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceSkillRequest) (*pb.EnhanceSkillResponse, error) {
|
||||
log.Printf("[WeaponService] EnhanceSkill: uuid=%s skillId=%d addLevel=%d", req.UserWeaponUuid, req.SkillId, req.AddLevelCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceSkill: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceSkill: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
groupRows := s.catalog.SkillGroupsByGroupId[wm.WeaponSkillGroupId]
|
||||
var skillGroup *masterdata.WeaponSkillGroupRow
|
||||
for i := range groupRows {
|
||||
if groupRows[i].SkillId == req.SkillId {
|
||||
skillGroup = &groupRows[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if skillGroup == nil {
|
||||
log.Printf("[WeaponService] EnhanceSkill: skillId=%d not found in group=%d", req.SkillId, wm.WeaponSkillGroupId)
|
||||
return
|
||||
}
|
||||
|
||||
skills := user.WeaponSkills[req.UserWeaponUuid]
|
||||
skillIdx := -1
|
||||
for i, sk := range skills {
|
||||
if sk.SlotNumber == skillGroup.SlotNumber {
|
||||
skillIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if skillIdx < 0 {
|
||||
log.Printf("[WeaponService] EnhanceSkill: slot=%d not found for weapon uuid=%s", skillGroup.SlotNumber, req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
maxLevelFunc, ok := s.catalog.SkillMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceSkill: no max skill level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
||||
return
|
||||
}
|
||||
maxLevel := maxLevelFunc.Evaluate(weapon.LimitBreakCount)
|
||||
|
||||
currentLevel := skills[skillIdx].Level
|
||||
addCount := req.AddLevelCount
|
||||
if currentLevel+addCount > maxLevel {
|
||||
addCount = maxLevel - currentLevel
|
||||
}
|
||||
if addCount <= 0 {
|
||||
log.Printf("[WeaponService] EnhanceSkill: already at max level %d", currentLevel)
|
||||
return
|
||||
}
|
||||
|
||||
enhanceMatId := skillGroup.WeaponSkillEnhancementMaterialId
|
||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||
key := [2]int32{enhanceMatId, lvl}
|
||||
mats := s.catalog.SkillEnhanceMats[key]
|
||||
for _, mat := range mats {
|
||||
cur := user.Materials[mat.MaterialId]
|
||||
cost := mat.Count
|
||||
if cur < cost {
|
||||
log.Printf("[WeaponService] EnhanceSkill: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
|
||||
cost = cur
|
||||
}
|
||||
user.Materials[mat.MaterialId] = cur - cost
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.SkillCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
goldCost := costFunc.Evaluate(lvl + 1)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
}
|
||||
}
|
||||
|
||||
skills[skillIdx].Level = currentLevel + addCount
|
||||
user.WeaponSkills[req.UserWeaponUuid] = skills
|
||||
log.Printf("[WeaponService] EnhanceSkill: skillId=%d level %d -> %d", req.SkillId, currentLevel, skills[skillIdx].Level)
|
||||
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon enhance skill: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
|
||||
|
||||
return &pb.EnhanceSkillResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.EnhanceAbilityRequest) (*pb.EnhanceAbilityResponse, error) {
|
||||
log.Printf("[WeaponService] EnhanceAbility: uuid=%s abilityId=%d addLevel=%d", req.UserWeaponUuid, req.AbilityId, req.AddLevelCount)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceAbility: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceAbility: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
groupRows := s.catalog.AbilityGroupsByGroupId[wm.WeaponAbilityGroupId]
|
||||
var abilityGroup *masterdata.WeaponAbilityGroupRow
|
||||
for i := range groupRows {
|
||||
if groupRows[i].AbilityId == req.AbilityId {
|
||||
abilityGroup = &groupRows[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if abilityGroup == nil {
|
||||
log.Printf("[WeaponService] EnhanceAbility: abilityId=%d not found in group=%d", req.AbilityId, wm.WeaponAbilityGroupId)
|
||||
return
|
||||
}
|
||||
|
||||
abilities := user.WeaponAbilities[req.UserWeaponUuid]
|
||||
abilityIdx := -1
|
||||
for i, ab := range abilities {
|
||||
if ab.SlotNumber == abilityGroup.SlotNumber {
|
||||
abilityIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if abilityIdx < 0 {
|
||||
log.Printf("[WeaponService] EnhanceAbility: slot=%d not found for weapon uuid=%s", abilityGroup.SlotNumber, req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
maxLevelFunc, ok := s.catalog.AbilityMaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceAbility: no max ability level func for enhanceId=%d", wm.WeaponSpecificEnhanceId)
|
||||
return
|
||||
}
|
||||
maxLevel := maxLevelFunc.Evaluate(weapon.LimitBreakCount)
|
||||
|
||||
currentLevel := abilities[abilityIdx].Level
|
||||
addCount := req.AddLevelCount
|
||||
if currentLevel+addCount > maxLevel {
|
||||
addCount = maxLevel - currentLevel
|
||||
}
|
||||
if addCount <= 0 {
|
||||
log.Printf("[WeaponService] EnhanceAbility: already at max level %d", currentLevel)
|
||||
return
|
||||
}
|
||||
|
||||
enhanceMatId := abilityGroup.WeaponAbilityEnhancementMaterialId
|
||||
for lvl := currentLevel; lvl < currentLevel+addCount; lvl++ {
|
||||
key := [2]int32{enhanceMatId, lvl}
|
||||
mats := s.catalog.AbilityEnhanceMats[key]
|
||||
for _, mat := range mats {
|
||||
cur := user.Materials[mat.MaterialId]
|
||||
cost := mat.Count
|
||||
if cur < cost {
|
||||
log.Printf("[WeaponService] EnhanceAbility: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
|
||||
cost = cur
|
||||
}
|
||||
user.Materials[mat.MaterialId] = cur - cost
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.AbilityCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
goldCost := costFunc.Evaluate(lvl + 1)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
}
|
||||
}
|
||||
|
||||
abilities[abilityIdx].Level = currentLevel + addCount
|
||||
user.WeaponAbilities[req.UserWeaponUuid] = abilities
|
||||
log.Printf("[WeaponService] EnhanceAbility: abilityId=%d level %d -> %d", req.AbilityId, currentLevel, abilities[abilityIdx].Level)
|
||||
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon enhance ability: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
|
||||
|
||||
return &pb.EnhanceAbilityResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.LimitBreakByMaterialRequest) (*pb.LimitBreakByMaterialResponse, error) {
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: uuid=%s materials=%v", req.UserWeaponUuid, req.Materials)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount {
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: already at max limit break %d", weapon.LimitBreakCount)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
||||
|
||||
totalMaterialCount := int32(0)
|
||||
for materialId, count := range req.Materials {
|
||||
if totalMaterialCount >= remaining {
|
||||
break
|
||||
}
|
||||
if count > remaining-totalMaterialCount {
|
||||
count = remaining - totalMaterialCount
|
||||
}
|
||||
cur := user.Materials[materialId]
|
||||
if cur < count {
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: insufficient material id=%d have=%d need=%d", materialId, cur, count)
|
||||
count = cur
|
||||
}
|
||||
user.Materials[materialId] = cur - count
|
||||
totalMaterialCount += count
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.LimitBreakCostByMaterialByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||
goldCost := costFunc.Evaluate(totalMaterialCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: gold cost=%d", goldCost)
|
||||
}
|
||||
|
||||
weapon.LimitBreakCount += totalMaterialCount
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
|
||||
note := user.WeaponNotes[weapon.WeaponId]
|
||||
if note.MaxLimitBreakCount < weapon.LimitBreakCount {
|
||||
note.MaxLimitBreakCount = weapon.LimitBreakCount
|
||||
note.LatestVersion = nowMillis
|
||||
user.WeaponNotes[weapon.WeaponId] = note
|
||||
}
|
||||
|
||||
log.Printf("[WeaponService] LimitBreakByMaterial: weaponId=%d limitBreak -> %d", weapon.WeaponId, weapon.LimitBreakCount)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon limit break by material: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, limitBreakDiffTables))
|
||||
|
||||
return &pb.LimitBreakByMaterialResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.LimitBreakByWeaponRequest) (*pb.LimitBreakByWeaponResponse, error) {
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
|
||||
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
|
||||
Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
if weapon.LimitBreakCount >= s.config.WeaponLimitBreakAvailableCount {
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: already at max limit break %d", weapon.LimitBreakCount)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
remaining := s.config.WeaponLimitBreakAvailableCount - weapon.LimitBreakCount
|
||||
|
||||
consumedCount := int32(0)
|
||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||
if consumedCount >= remaining {
|
||||
break
|
||||
}
|
||||
|
||||
matWeapon, ok := user.Weapons[uuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: material weapon uuid=%s not found, skipping", uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||
for itemId, count := range medals {
|
||||
user.ConsumableItems[itemId] += count
|
||||
}
|
||||
}
|
||||
|
||||
delete(user.Weapons, uuid)
|
||||
delete(user.WeaponSkills, uuid)
|
||||
delete(user.WeaponAbilities, uuid)
|
||||
consumedCount++
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.LimitBreakCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
||||
goldCost := costFunc.Evaluate(consumedCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: gold cost=%d", goldCost)
|
||||
}
|
||||
|
||||
weapon.LimitBreakCount += consumedCount
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
|
||||
note := user.WeaponNotes[weapon.WeaponId]
|
||||
if note.MaxLimitBreakCount < weapon.LimitBreakCount {
|
||||
note.MaxLimitBreakCount = weapon.LimitBreakCount
|
||||
note.LatestVersion = nowMillis
|
||||
user.WeaponNotes[weapon.WeaponId] = note
|
||||
}
|
||||
|
||||
log.Printf("[WeaponService] LimitBreakByWeapon: weaponId=%d limitBreak -> %d (consumed %d weapons)", weapon.WeaponId, weapon.LimitBreakCount, consumedCount)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon limit break by weapon: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), limitBreakDiffTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.LimitBreakByWeaponResponse{DiffUserData: diff}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.EnhanceByWeaponRequest) (*pb.EnhanceByWeaponResponse, error) {
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: uuid=%s materialUuids=%v", req.UserWeaponUuid, req.MaterialUserWeaponUuids)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
oldUser, _ := s.users.SnapshotUser(userId)
|
||||
tracker := userdata.NewDeleteTracker().
|
||||
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
|
||||
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
|
||||
Track("IUserWeaponAbility", oldUser, userdata.SortedWeaponAbilityRecords, []string{"userId", "userWeaponUuid", "slotNumber"})
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
weapon, ok := user.Weapons[req.UserWeaponUuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: weapon uuid=%s not found", req.UserWeaponUuid)
|
||||
return
|
||||
}
|
||||
|
||||
wm, ok := s.catalog.Weapons[weapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: weapon master id=%d not found", weapon.WeaponId)
|
||||
return
|
||||
}
|
||||
|
||||
totalExp := int32(0)
|
||||
consumedCount := int32(0)
|
||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||
matWeapon, ok := user.Weapons[uuid]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: material weapon uuid=%s not found, skipping", uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
matMaster, ok := s.catalog.Weapons[matWeapon.WeaponId]
|
||||
if !ok {
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: material weapon master id=%d not found, skipping", matWeapon.WeaponId)
|
||||
continue
|
||||
}
|
||||
|
||||
baseExp := s.catalog.BaseExpByEnhanceId[matMaster.WeaponSpecificEnhanceId]
|
||||
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
||||
baseExp = baseExp * s.config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||
}
|
||||
totalExp += baseExp
|
||||
|
||||
if medals, ok := s.catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||
for itemId, count := range medals {
|
||||
user.ConsumableItems[itemId] += count
|
||||
}
|
||||
}
|
||||
|
||||
delete(user.Weapons, uuid)
|
||||
delete(user.WeaponSkills, uuid)
|
||||
delete(user.WeaponAbilities, uuid)
|
||||
consumedCount++
|
||||
}
|
||||
|
||||
if costFunc, ok := s.catalog.EnhanceCostByWeaponByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && consumedCount > 0 {
|
||||
goldCost := costFunc.Evaluate(consumedCount)
|
||||
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= goldCost
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: gold cost=%d (weapons=%d)", goldCost, consumedCount)
|
||||
}
|
||||
|
||||
weapon.Exp += totalExp
|
||||
if thresholds, ok := s.catalog.ExpByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||
}
|
||||
|
||||
weapon.LatestVersion = nowMillis
|
||||
user.Weapons[req.UserWeaponUuid] = weapon
|
||||
log.Printf("[WeaponService] EnhanceByWeapon: weaponId=%d +%d exp -> total=%d level=%d", weapon.WeaponId, totalExp, weapon.Exp, weapon.Level)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), weaponDiffTables)
|
||||
diff := tracker.Apply(snapshot, tables)
|
||||
|
||||
return &pb.EnhanceByWeaponResponse{
|
||||
IsGreatSuccess: false,
|
||||
SurplusEnhanceWeapon: []string{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WeaponServiceServer) checkEvolutionStoryUnlocks(user *store.UserState, weaponId int32, nowMillis int64) {
|
||||
wm, ok := s.catalog.Weapons[weaponId]
|
||||
if !ok || wm.WeaponStoryReleaseConditionGroupId == 0 {
|
||||
return
|
||||
}
|
||||
evoOrder, hasEvo := s.catalog.EvolutionOrder[weaponId]
|
||||
conditions := s.catalog.ReleaseConditionsByGroupId[wm.WeaponStoryReleaseConditionGroupId]
|
||||
for _, cond := range conditions {
|
||||
switch cond.WeaponStoryReleaseConditionType {
|
||||
case model.WeaponStoryReleaseConditionTypeReachSpecifiedEvolutionCount:
|
||||
if hasEvo && evoOrder >= cond.ConditionValue {
|
||||
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
}
|
||||
case model.WeaponStoryReleaseConditionTypeAcquisition:
|
||||
store.GrantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
)
|
||||
|
||||
func DeductPrice(user *UserState, priceType, priceId, amount int32) error {
|
||||
switch priceType {
|
||||
case model.PriceTypeConsumableItem:
|
||||
cur := user.ConsumableItems[priceId]
|
||||
if cur < amount {
|
||||
return fmt.Errorf("insufficient consumable %d: have %d, need %d", priceId, cur, amount)
|
||||
}
|
||||
user.ConsumableItems[priceId] = cur - amount
|
||||
case model.PriceTypeGem:
|
||||
total := user.Gem.FreeGem + user.Gem.PaidGem
|
||||
if total < amount {
|
||||
return fmt.Errorf("insufficient gems: have %d, need %d", total, amount)
|
||||
}
|
||||
if user.Gem.FreeGem >= amount {
|
||||
user.Gem.FreeGem -= amount
|
||||
} else {
|
||||
amount -= user.Gem.FreeGem
|
||||
user.Gem.FreeGem = 0
|
||||
user.Gem.PaidGem -= amount
|
||||
}
|
||||
case model.PriceTypePaidGem:
|
||||
if user.Gem.PaidGem < amount {
|
||||
return fmt.Errorf("insufficient paid gems: have %d, need %d", user.Gem.PaidGem, amount)
|
||||
}
|
||||
user.Gem.PaidGem -= amount
|
||||
case model.PriceTypePlatformPayment:
|
||||
// real-money purchase -- treat as free on private server
|
||||
default:
|
||||
log.Printf("[DeductPrice] unhandled priceType=%d priceId=%d amount=%d", priceType, priceId, amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeductPossession(user *UserState, possessionType model.PossessionType, possessionId, count int32) {
|
||||
switch possessionType {
|
||||
case model.PossessionTypeMaterial:
|
||||
user.Materials[possessionId] -= count
|
||||
if user.Materials[possessionId] <= 0 {
|
||||
delete(user.Materials, possessionId)
|
||||
}
|
||||
case model.PossessionTypeConsumableItem:
|
||||
user.ConsumableItems[possessionId] -= count
|
||||
if user.ConsumableItems[possessionId] <= 0 {
|
||||
delete(user.ConsumableItems, possessionId)
|
||||
}
|
||||
case model.PossessionTypePaidGem:
|
||||
user.Gem.PaidGem -= count
|
||||
case model.PossessionTypeFreeGem:
|
||||
user.Gem.FreeGem -= count
|
||||
default:
|
||||
log.Printf("[DeductPossession] unhandled type=%d id=%d count=%d", possessionType, possessionId, count)
|
||||
}
|
||||
}
|
||||
|
||||
func GrantPossession(user *UserState, possessionType model.PossessionType, possessionId, count int32) {
|
||||
switch possessionType {
|
||||
case model.PossessionTypeMaterial:
|
||||
user.Materials[possessionId] += count
|
||||
case model.PossessionTypeConsumableItem:
|
||||
user.ConsumableItems[possessionId] += count
|
||||
case model.PossessionTypePaidGem:
|
||||
user.Gem.PaidGem += count
|
||||
case model.PossessionTypeFreeGem:
|
||||
user.Gem.FreeGem += count
|
||||
case model.PossessionTypeImportantItem:
|
||||
user.ImportantItems[possessionId] += count
|
||||
case model.PossessionTypePremiumItem:
|
||||
user.PremiumItems[possessionId] = gametime.NowMillis()
|
||||
default:
|
||||
log.Printf("[GrantPossession] unhandled type=%d id=%d count=%d", possessionType, possessionId, count)
|
||||
}
|
||||
}
|
||||
|
||||
type CostumeRef struct {
|
||||
CharacterId int32
|
||||
}
|
||||
|
||||
type WeaponRef struct {
|
||||
WeaponSkillGroupId int32
|
||||
WeaponAbilityGroupId int32
|
||||
WeaponStoryReleaseConditionGroupId int32
|
||||
}
|
||||
|
||||
type WeaponStoryReleaseCond struct {
|
||||
StoryIndex int32
|
||||
WeaponStoryReleaseConditionType model.WeaponStoryReleaseConditionType
|
||||
ConditionValue int32
|
||||
}
|
||||
|
||||
type PossessionGranter struct {
|
||||
CostumeById map[int32]CostumeRef
|
||||
WeaponById map[int32]WeaponRef
|
||||
WeaponSkillSlots map[int32][]int32
|
||||
WeaponAbilitySlots map[int32][]int32
|
||||
ReleaseConditions map[int32][]WeaponStoryReleaseCond
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) GrantFull(user *UserState, possessionType model.PossessionType, possessionId, count int32, nowMillis int64) {
|
||||
switch possessionType {
|
||||
case model.PossessionTypeCostume, model.PossessionTypeCostumeEnhanced:
|
||||
g.GrantCostume(user, possessionId, nowMillis)
|
||||
case model.PossessionTypeWeapon, model.PossessionTypeWeaponEnhanced:
|
||||
g.GrantWeapon(user, possessionId, nowMillis)
|
||||
default:
|
||||
GrantPossession(user, possessionType, possessionId, count)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) GrantCostume(user *UserState, costumeId int32, nowMillis int64) {
|
||||
for _, row := range user.Costumes {
|
||||
if row.CostumeId == costumeId {
|
||||
return
|
||||
}
|
||||
}
|
||||
if cm, ok := g.CostumeById[costumeId]; ok {
|
||||
if _, exists := user.Characters[cm.CharacterId]; !exists {
|
||||
user.Characters[cm.CharacterId] = CharacterState{
|
||||
CharacterId: cm.CharacterId,
|
||||
Level: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
key := fmt.Sprintf("reward-costume-%d", costumeId)
|
||||
user.Costumes[key] = CostumeState{
|
||||
UserCostumeUuid: key,
|
||||
CostumeId: costumeId,
|
||||
Level: 1,
|
||||
HeadupDisplayViewId: 1,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
user.CostumeActiveSkills[key] = CostumeActiveSkillState{
|
||||
UserCostumeUuid: key,
|
||||
Level: 1,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
||||
key := fmt.Sprintf("reward-weapon-%d-%d", weaponId, nowMillis)
|
||||
user.Weapons[key] = WeaponState{
|
||||
UserWeaponUuid: key,
|
||||
WeaponId: weaponId,
|
||||
Level: 1,
|
||||
AcquisitionDatetime: nowMillis,
|
||||
}
|
||||
if _, exists := user.WeaponNotes[weaponId]; !exists {
|
||||
user.WeaponNotes[weaponId] = WeaponNoteState{
|
||||
WeaponId: weaponId,
|
||||
MaxLevel: 1,
|
||||
MaxLimitBreakCount: 0,
|
||||
FirstAcquisitionDatetime: nowMillis,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
weapon, ok := g.WeaponById[weaponId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
g.populateWeaponSkillsAbilities(user, key, weapon)
|
||||
if weapon.WeaponStoryReleaseConditionGroupId != 0 {
|
||||
for _, cond := range g.ReleaseConditions[weapon.WeaponStoryReleaseConditionGroupId] {
|
||||
switch cond.WeaponStoryReleaseConditionType {
|
||||
case model.WeaponStoryReleaseConditionTypeAcquisition:
|
||||
grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
case model.WeaponStoryReleaseConditionTypeQuestClear:
|
||||
if qs, ok := user.Quests[cond.ConditionValue]; ok && qs.QuestStateType == model.UserQuestStateTypeCleared {
|
||||
grantWeaponStoryUnlock(user, weaponId, cond.StoryIndex, nowMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PossessionGranter) populateWeaponSkillsAbilities(user *UserState, weaponUuid string, weapon WeaponRef) {
|
||||
if slots, ok := g.WeaponSkillSlots[weapon.WeaponSkillGroupId]; ok {
|
||||
skills := make([]WeaponSkillState, len(slots))
|
||||
for i, slot := range slots {
|
||||
skills[i] = WeaponSkillState{
|
||||
UserWeaponUuid: weaponUuid,
|
||||
SlotNumber: slot,
|
||||
Level: 1,
|
||||
}
|
||||
}
|
||||
user.WeaponSkills[weaponUuid] = skills
|
||||
}
|
||||
if slots, ok := g.WeaponAbilitySlots[weapon.WeaponAbilityGroupId]; ok {
|
||||
abilities := make([]WeaponAbilityState, len(slots))
|
||||
for i, slot := range slots {
|
||||
abilities[i] = WeaponAbilityState{
|
||||
UserWeaponUuid: weaponUuid,
|
||||
SlotNumber: slot,
|
||||
Level: 1,
|
||||
}
|
||||
}
|
||||
user.WeaponAbilities[weaponUuid] = abilities
|
||||
}
|
||||
}
|
||||
|
||||
func GrantWeaponStoryUnlock(user *UserState, weaponId, storyIndex int32, nowMillis int64) {
|
||||
grantWeaponStoryUnlock(user, weaponId, storyIndex, nowMillis)
|
||||
}
|
||||
|
||||
func grantWeaponStoryUnlock(user *UserState, weaponId, storyIndex int32, nowMillis int64) {
|
||||
hasWeapon := false
|
||||
for _, row := range user.Weapons {
|
||||
if row.WeaponId == weaponId {
|
||||
hasWeapon = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasWeapon {
|
||||
log.Printf("[grantWeaponStoryUnlock] skipping weaponId=%d (weapon not in user.Weapons)", weaponId)
|
||||
return
|
||||
}
|
||||
if user.WeaponStories == nil {
|
||||
user.WeaponStories = make(map[int32]WeaponStoryState)
|
||||
}
|
||||
cur := user.WeaponStories[weaponId]
|
||||
if storyIndex <= cur.ReleasedMaxStoryIndex {
|
||||
return
|
||||
}
|
||||
user.WeaponStories[weaponId] = WeaponStoryState{
|
||||
WeaponId: weaponId,
|
||||
ReleasedMaxStoryIndex: storyIndex,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDefaultDeck(user *UserState, nowMillis int64) {
|
||||
if len(user.Costumes) == 0 || len(user.Decks) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
costumeUuid := FirstSortedKey(user.Costumes)
|
||||
weaponUuid := FirstSortedKey(user.Weapons)
|
||||
companionUuid := FirstSortedKey(user.Companions)
|
||||
|
||||
dcUuid := "default-deck-character-0001"
|
||||
user.DeckCharacters[dcUuid] = DeckCharacterState{
|
||||
UserDeckCharacterUuid: dcUuid,
|
||||
UserCostumeUuid: costumeUuid,
|
||||
MainUserWeaponUuid: weaponUuid,
|
||||
UserCompanionUuid: companionUuid,
|
||||
Power: 100,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
user.Decks[DeckKey{DeckType: model.DeckTypeQuest, UserDeckNumber: 1}] = DeckState{
|
||||
DeckType: model.DeckTypeQuest,
|
||||
UserDeckNumber: 1,
|
||||
UserDeckCharacterUuid01: dcUuid,
|
||||
Name: "Deck 1",
|
||||
Power: 100,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
|
||||
if _, exists := user.DeckTypeNotes[model.DeckTypeQuest]; !exists {
|
||||
user.DeckTypeNotes[model.DeckTypeQuest] = DeckTypeNoteState{
|
||||
DeckType: model.DeckTypeQuest,
|
||||
MaxDeckPower: 100,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FirstSortedKey[V any](m map[string]V) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
func ApplyDeckReplacement(user *UserState, deckType model.DeckType, userDeckNumber int32, slots []DeckCharacterInput, nowMillis int64) {
|
||||
deckKey := DeckKey{DeckType: deckType, UserDeckNumber: userDeckNumber}
|
||||
deck := user.Decks[deckKey]
|
||||
deck.DeckType = deckType
|
||||
deck.UserDeckNumber = userDeckNumber
|
||||
if deck.Name == "" {
|
||||
deck.Name = fmt.Sprintf("Deck %d", userDeckNumber)
|
||||
}
|
||||
if deck.Power == 0 {
|
||||
deck.Power = 100
|
||||
}
|
||||
|
||||
uuids := []*string{&deck.UserDeckCharacterUuid01, &deck.UserDeckCharacterUuid02, &deck.UserDeckCharacterUuid03}
|
||||
for i, uuid := range uuids {
|
||||
if i >= len(slots) || slots[i].UserCostumeUuid == "" {
|
||||
*uuid = ""
|
||||
continue
|
||||
}
|
||||
slot := slots[i]
|
||||
dcUuid := fmt.Sprintf("deck-%d-%d-%d", deckType, userDeckNumber, i+1)
|
||||
dc := user.DeckCharacters[dcUuid]
|
||||
dc.UserDeckCharacterUuid = dcUuid
|
||||
dc.UserCostumeUuid = slot.UserCostumeUuid
|
||||
dc.MainUserWeaponUuid = slot.MainUserWeaponUuid
|
||||
dc.UserCompanionUuid = slot.UserCompanionUuid
|
||||
dc.UserThoughtUuid = slot.UserThoughtUuid
|
||||
dc.DressupCostumeId = slot.DressupCostumeId
|
||||
dc.LatestVersion = nowMillis
|
||||
user.DeckCharacters[dcUuid] = dc
|
||||
user.DeckSubWeapons[dcUuid] = slot.SubWeaponUuids
|
||||
user.DeckParts[dcUuid] = slot.PartsUuids
|
||||
*uuid = dcUuid
|
||||
}
|
||||
|
||||
deck.LatestVersion = nowMillis
|
||||
user.Decks[deckKey] = deck
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func marshalKey(vals ...int64) []byte {
|
||||
b := strconv.AppendInt(nil, vals[0], 10)
|
||||
for _, v := range vals[1:] {
|
||||
b = append(b, ':')
|
||||
b = strconv.AppendInt(b, v, 10)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func unmarshalKey(text []byte, name string, n int) ([]int64, error) {
|
||||
parts := strings.Split(string(text), ":")
|
||||
if len(parts) != n {
|
||||
return nil, fmt.Errorf("invalid %s: %s", name, text)
|
||||
}
|
||||
out := make([]int64, n)
|
||||
for i, p := range parts {
|
||||
v, err := strconv.ParseInt(p, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[i] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func cloneUserState(u store.UserState) store.UserState {
|
||||
out := u
|
||||
out.Tutorials = maps.Clone(u.Tutorials)
|
||||
out.Characters = maps.Clone(u.Characters)
|
||||
out.Costumes = maps.Clone(u.Costumes)
|
||||
out.Weapons = maps.Clone(u.Weapons)
|
||||
out.Companions = maps.Clone(u.Companions)
|
||||
out.Thoughts = maps.Clone(u.Thoughts)
|
||||
out.DeckCharacters = maps.Clone(u.DeckCharacters)
|
||||
out.DeckSubWeapons = maps.Clone(u.DeckSubWeapons)
|
||||
out.DeckParts = cloneSliceMap(u.DeckParts)
|
||||
out.Decks = maps.Clone(u.Decks)
|
||||
out.Quests = maps.Clone(u.Quests)
|
||||
out.QuestMissions = maps.Clone(u.QuestMissions)
|
||||
out.WeaponStories = maps.Clone(u.WeaponStories)
|
||||
out.Missions = maps.Clone(u.Missions)
|
||||
out.Gimmick = store.GimmickState{
|
||||
Progress: maps.Clone(u.Gimmick.Progress),
|
||||
OrnamentProgress: maps.Clone(u.Gimmick.OrnamentProgress),
|
||||
Sequences: maps.Clone(u.Gimmick.Sequences),
|
||||
Unlocks: maps.Clone(u.Gimmick.Unlocks),
|
||||
}
|
||||
out.CageOrnamentRewards = maps.Clone(u.CageOrnamentRewards)
|
||||
out.ConsumableItems = maps.Clone(u.ConsumableItems)
|
||||
out.Materials = maps.Clone(u.Materials)
|
||||
out.Parts = maps.Clone(u.Parts)
|
||||
out.PartsGroupNotes = maps.Clone(u.PartsGroupNotes)
|
||||
out.PartsPresets = maps.Clone(u.PartsPresets)
|
||||
out.ImportantItems = maps.Clone(u.ImportantItems)
|
||||
out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills)
|
||||
out.WeaponSkills = cloneSliceMap(u.WeaponSkills)
|
||||
out.WeaponAbilities = cloneSliceMap(u.WeaponAbilities)
|
||||
out.DeckTypeNotes = maps.Clone(u.DeckTypeNotes)
|
||||
out.WeaponNotes = maps.Clone(u.WeaponNotes)
|
||||
out.NaviCutInPlayed = maps.Clone(u.NaviCutInPlayed)
|
||||
out.ViewedMovies = maps.Clone(u.ViewedMovies)
|
||||
out.ContentsStories = maps.Clone(u.ContentsStories)
|
||||
out.DrawnOmikuji = maps.Clone(u.DrawnOmikuji)
|
||||
out.PremiumItems = maps.Clone(u.PremiumItems)
|
||||
out.DokanConfirmed = maps.Clone(u.DokanConfirmed)
|
||||
out.ShopItems = maps.Clone(u.ShopItems)
|
||||
out.ShopReplaceableLineup = maps.Clone(u.ShopReplaceableLineup)
|
||||
out.Explore = u.Explore
|
||||
out.ExploreScores = maps.Clone(u.ExploreScores)
|
||||
out.Gacha = store.GachaState{
|
||||
RewardAvailable: u.Gacha.RewardAvailable,
|
||||
TodaysCurrentDrawCount: u.Gacha.TodaysCurrentDrawCount,
|
||||
DailyMaxCount: u.Gacha.DailyMaxCount,
|
||||
LastRewardDrawDate: u.Gacha.LastRewardDrawDate,
|
||||
ConvertedGachaMedal: store.ConvertedGachaMedalState{
|
||||
ConvertedMedalPossession: append([]store.ConsumableItemState(nil), u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession...),
|
||||
ObtainPossession: cloneConsumableItemPtr(u.Gacha.ConvertedGachaMedal.ObtainPossession),
|
||||
},
|
||||
BannerStates: cloneBannerStates(u.Gacha.BannerStates),
|
||||
}
|
||||
out.Gifts = store.GiftState{
|
||||
NotReceived: cloneNotReceivedGifts(u.Gifts.NotReceived),
|
||||
Received: cloneReceivedGifts(u.Gifts.Received),
|
||||
}
|
||||
out.Battle = u.Battle
|
||||
out.CostumeAwakenStatusUps = maps.Clone(u.CostumeAwakenStatusUps)
|
||||
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
|
||||
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneGachaCatalogEntry(entry store.GachaCatalogEntry) store.GachaCatalogEntry {
|
||||
out := entry
|
||||
out.PricePhases = append([]store.GachaPricePhaseEntry(nil), entry.PricePhases...)
|
||||
out.PromotionItems = append([]store.GachaPromotionItem(nil), entry.PromotionItems...)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneBannerStates(m map[int32]store.GachaBannerState) map[int32]store.GachaBannerState {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[int32]store.GachaBannerState, len(m))
|
||||
for k, v := range m {
|
||||
bs := v
|
||||
bs.BoxDrewCounts = maps.Clone(v.BoxDrewCounts)
|
||||
out[k] = bs
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneConsumableItemPtr(item *store.ConsumableItemState) *store.ConsumableItemState {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
out := *item
|
||||
return &out
|
||||
}
|
||||
|
||||
func cloneNotReceivedGifts(gifts []store.NotReceivedGiftState) []store.NotReceivedGiftState {
|
||||
out := make([]store.NotReceivedGiftState, len(gifts))
|
||||
for i, gift := range gifts {
|
||||
out[i] = store.NotReceivedGiftState{
|
||||
GiftCommon: store.GiftCommonState{
|
||||
PossessionType: gift.GiftCommon.PossessionType,
|
||||
PossessionId: gift.GiftCommon.PossessionId,
|
||||
Count: gift.GiftCommon.Count,
|
||||
GrantDatetime: gift.GiftCommon.GrantDatetime,
|
||||
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
|
||||
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
|
||||
},
|
||||
ExpirationDatetime: gift.ExpirationDatetime,
|
||||
UserGiftUuid: gift.UserGiftUuid,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneSliceMap[T any](m map[string][]T) map[string][]T {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]T, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = append([]T(nil), v...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneReceivedGifts(gifts []store.ReceivedGiftState) []store.ReceivedGiftState {
|
||||
out := make([]store.ReceivedGiftState, len(gifts))
|
||||
for i, gift := range gifts {
|
||||
out[i] = store.ReceivedGiftState{
|
||||
GiftCommon: store.GiftCommonState{
|
||||
PossessionType: gift.GiftCommon.PossessionType,
|
||||
PossessionId: gift.GiftCommon.PossessionId,
|
||||
Count: gift.GiftCommon.Count,
|
||||
GrantDatetime: gift.GiftCommon.GrantDatetime,
|
||||
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
|
||||
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
|
||||
},
|
||||
ReceivedDatetime: gift.ReceivedDatetime,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
type Option func(*MemoryStore)
|
||||
|
||||
func WithSnapshotDir(dir string) Option {
|
||||
return func(s *MemoryStore) {
|
||||
s.snapshotDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithSceneId(sceneId int32) Option {
|
||||
return func(s *MemoryStore) {
|
||||
s.bootstrapSceneId = sceneId
|
||||
}
|
||||
}
|
||||
|
||||
func WithStarterItems(v bool) Option {
|
||||
return func(s *MemoryStore) {
|
||||
s.starterItems = v
|
||||
}
|
||||
}
|
||||
|
||||
type MemoryStore struct {
|
||||
mu sync.RWMutex
|
||||
clock store.Clock
|
||||
bootstrapSceneId int32
|
||||
snapshotDir string
|
||||
starterItems bool
|
||||
lastSnapshotSceneId int32
|
||||
nextUserId int64
|
||||
users map[int64]*store.UserState
|
||||
userIdsByUuid map[string]int64
|
||||
sessionToUserId map[string]int64
|
||||
sessions map[string]store.SessionState
|
||||
gachaCatalog map[int32]store.GachaCatalogEntry
|
||||
}
|
||||
|
||||
var (
|
||||
_ store.UserRepository = (*MemoryStore)(nil)
|
||||
_ store.SessionRepository = (*MemoryStore)(nil)
|
||||
_ store.GachaRepository = (*MemoryStore)(nil)
|
||||
)
|
||||
|
||||
func New(clock store.Clock, options ...Option) *MemoryStore {
|
||||
if clock == nil {
|
||||
clock = time.Now
|
||||
}
|
||||
s := &MemoryStore{
|
||||
clock: clock,
|
||||
nextUserId: defaultUserId,
|
||||
users: make(map[int64]*store.UserState),
|
||||
userIdsByUuid: make(map[string]int64),
|
||||
sessionToUserId: make(map[string]int64),
|
||||
sessions: make(map[string]store.SessionState),
|
||||
gachaCatalog: make(map[int32]store.GachaCatalogEntry),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *MemoryStore) EnsureUser(uuid string) (store.UserState, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return cloneUserState(*s.getOrCreateLocked(normalizeUUID(uuid))), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) CreateSession(uuid string, ttl time.Duration) (store.UserState, store.SessionState, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
user := s.getOrCreateLocked(normalizeUUID(uuid))
|
||||
now := s.clock()
|
||||
session := store.SessionState{
|
||||
SessionKey: fmt.Sprintf("session_%d_%d", user.UserId, now.UnixNano()),
|
||||
UserId: user.UserId,
|
||||
Uuid: user.Uuid,
|
||||
ExpireAt: now.Add(ttl),
|
||||
}
|
||||
|
||||
s.sessionToUserId[session.SessionKey] = user.UserId
|
||||
s.sessions[session.SessionKey] = session
|
||||
|
||||
return cloneUserState(*user), session, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) ResolveUserId(sessionKey string) (int64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
userId, ok := s.sessionToUserId[sessionKey]
|
||||
if !ok {
|
||||
return 0, store.ErrNotFound
|
||||
}
|
||||
return userId, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) SnapshotUser(userId int64) (store.UserState, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
user, ok := s.users[userId]
|
||||
if !ok {
|
||||
return store.UserState{}, store.ErrNotFound
|
||||
}
|
||||
return cloneUserState(*user), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) UpdateUser(userId int64, mutate func(*store.UserState)) (store.UserState, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
user, ok := s.users[userId]
|
||||
if !ok {
|
||||
return store.UserState{}, store.ErrNotFound
|
||||
}
|
||||
mutate(user)
|
||||
sceneId := user.MainQuest.CurrentQuestSceneId
|
||||
if s.snapshotDir != "" && sceneId != 0 && sceneId != s.lastSnapshotSceneId {
|
||||
saveSnapshot(user, s.snapshotDir)
|
||||
s.lastSnapshotSceneId = sceneId
|
||||
}
|
||||
return cloneUserState(*user), nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) DefaultUserId() (int64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if _, ok := s.users[defaultUserId]; ok {
|
||||
return defaultUserId, nil
|
||||
}
|
||||
if len(s.users) == 0 {
|
||||
return defaultUserId, nil
|
||||
}
|
||||
|
||||
var minUserId int64
|
||||
for userId := range s.users {
|
||||
if minUserId == 0 || userId < minUserId {
|
||||
minUserId = userId
|
||||
}
|
||||
}
|
||||
return minUserId, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) SnapshotCatalog() ([]store.GachaCatalogEntry, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
out := make([]store.GachaCatalogEntry, 0, len(s.gachaCatalog))
|
||||
for _, entry := range s.gachaCatalog {
|
||||
out = append(out, cloneGachaCatalogEntry(entry))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) ReplaceCatalog(entries []store.GachaCatalogEntry) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.gachaCatalog = make(map[int32]store.GachaCatalogEntry, len(entries))
|
||||
for _, entry := range entries {
|
||||
s.gachaCatalog[entry.GachaId] = cloneGachaCatalogEntry(entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) getOrCreateLocked(uuid string) *store.UserState {
|
||||
if userId, ok := s.userIdsByUuid[uuid]; ok {
|
||||
return s.users[userId]
|
||||
}
|
||||
|
||||
userId := s.nextUserId
|
||||
s.nextUserId++
|
||||
|
||||
user := seedUserState(userId, uuid, s.clock().UnixMilli(), s.bootstrapSceneId, s.snapshotDir, s.starterItems)
|
||||
s.users[userId] = user
|
||||
s.userIdsByUuid[uuid] = userId
|
||||
return user
|
||||
}
|
||||
|
||||
func normalizeUUID(uuid string) string {
|
||||
uuid = strings.TrimSpace(uuid)
|
||||
if uuid == "" {
|
||||
return defaultUUID
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUUID = "default-user"
|
||||
defaultUserId = int64(1001)
|
||||
|
||||
starterMissionId = int32(1)
|
||||
starterMainQuestRouteId = int32(1)
|
||||
starterMainQuestSeasonId = int32(1)
|
||||
missionInProgress = int32(1)
|
||||
giftUUIDPrefix = "default-gift"
|
||||
|
||||
defaultBirthYear = int32(2000)
|
||||
defaultBirthMonth = int32(1)
|
||||
defaultBackupToken = "mock-backup-token"
|
||||
defaultChargeMoneyThisMonth = int64(0)
|
||||
)
|
||||
|
||||
type starterItemDef struct {
|
||||
Type model.PossessionType
|
||||
Id int32
|
||||
Qty int32
|
||||
}
|
||||
|
||||
var defaultStarterItems = []starterItemDef{
|
||||
{Type: model.PossessionTypeFreeGem, Id: 0, Qty: 300},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: 9001, Qty: 1000},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: model.ConsumableIdChapterTicket, Qty: 1000},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: 5001, Qty: 1000},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: 5002, Qty: 1000},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: 5003, Qty: 1000},
|
||||
{Type: model.PossessionTypeConsumableItem, Id: 1009, Qty: 1000},
|
||||
}
|
||||
|
||||
func seedUserState(userId int64, uuid string, nowMillis int64, sceneId int32, snapshotDir string, grantStarterItems bool) *store.UserState {
|
||||
if sceneId != 0 && snapshotDir != "" {
|
||||
user, err := loadSnapshot(snapshotDir, sceneId)
|
||||
if err != nil {
|
||||
log.Fatalf("[bootstrap] no snapshot for scene=%d: %v", sceneId, err)
|
||||
}
|
||||
log.Printf("[bootstrap] loaded snapshot for scene=%d", sceneId)
|
||||
if grantStarterItems {
|
||||
applyStarterItems(user)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
user := &store.UserState{
|
||||
UserId: userId,
|
||||
Uuid: uuid,
|
||||
PlayerId: userId,
|
||||
OsType: 2,
|
||||
PlatformType: 2,
|
||||
UserRestrictionType: 0,
|
||||
RegisterDatetime: nowMillis,
|
||||
GameStartDatetime: nowMillis,
|
||||
LatestVersion: 0,
|
||||
BirthYear: defaultBirthYear,
|
||||
BirthMonth: defaultBirthMonth,
|
||||
BackupToken: defaultBackupToken,
|
||||
ChargeMoneyThisMonth: defaultChargeMoneyThisMonth,
|
||||
Setting: store.UserSettingState{
|
||||
IsNotifyPurchaseAlert: false,
|
||||
LatestVersion: 0,
|
||||
},
|
||||
Status: store.UserStatusState{
|
||||
Level: 1,
|
||||
Exp: 0,
|
||||
StaminaMilliValue: 50000,
|
||||
StaminaUpdateDatetime: nowMillis,
|
||||
LatestVersion: 0,
|
||||
},
|
||||
Gem: store.UserGemState{
|
||||
PaidGem: 10000,
|
||||
FreeGem: 10000,
|
||||
},
|
||||
Profile: store.UserProfileState{
|
||||
Name: "",
|
||||
NameUpdateDatetime: 0,
|
||||
Message: "",
|
||||
MessageUpdateDatetime: nowMillis,
|
||||
FavoriteCostumeId: 0,
|
||||
FavoriteCostumeIdUpdateDatetime: nowMillis,
|
||||
LatestVersion: 0,
|
||||
},
|
||||
Login: store.UserLoginState{
|
||||
TotalLoginCount: 1,
|
||||
ContinualLoginCount: 1,
|
||||
MaxContinualLoginCount: 1,
|
||||
LastLoginDatetime: nowMillis,
|
||||
LastComebackLoginDatetime: 0,
|
||||
LatestVersion: 0,
|
||||
},
|
||||
LoginBonus: store.UserLoginBonusState{
|
||||
LoginBonusId: 1,
|
||||
CurrentPageNumber: 1,
|
||||
CurrentStampNumber: 0,
|
||||
LatestRewardReceiveDatetime: 0,
|
||||
LatestVersion: 0,
|
||||
},
|
||||
Tutorials: map[int32]store.TutorialProgressState{
|
||||
1: {TutorialType: 1},
|
||||
},
|
||||
Battle: store.BattleState{},
|
||||
Gifts: store.GiftState{
|
||||
NotReceived: []store.NotReceivedGiftState{
|
||||
{
|
||||
GiftCommon: store.GiftCommonState{
|
||||
PossessionType: int32(model.PossessionTypeFreeGem),
|
||||
PossessionId: 0,
|
||||
Count: 300,
|
||||
GrantDatetime: nowMillis,
|
||||
},
|
||||
ExpirationDatetime: nowMillis + int64((7*24*time.Hour)/time.Millisecond),
|
||||
UserGiftUuid: fmt.Sprintf("%s-%d-1", giftUUIDPrefix, userId),
|
||||
},
|
||||
},
|
||||
Received: []store.ReceivedGiftState{},
|
||||
},
|
||||
Gacha: store.GachaState{
|
||||
ConvertedGachaMedal: store.ConvertedGachaMedalState{
|
||||
ConvertedMedalPossession: []store.ConsumableItemState{},
|
||||
},
|
||||
BannerStates: make(map[int32]store.GachaBannerState),
|
||||
},
|
||||
MainQuest: store.MainQuestState{
|
||||
CurrentMainQuestRouteId: starterMainQuestRouteId,
|
||||
MainQuestSeasonId: starterMainQuestSeasonId,
|
||||
},
|
||||
Notifications: store.NotificationState{
|
||||
GiftNotReceiveCount: 1,
|
||||
},
|
||||
Characters: make(map[int32]store.CharacterState),
|
||||
Costumes: make(map[string]store.CostumeState),
|
||||
Weapons: make(map[string]store.WeaponState),
|
||||
Companions: make(map[string]store.CompanionState),
|
||||
DeckCharacters: make(map[string]store.DeckCharacterState),
|
||||
Decks: make(map[store.DeckKey]store.DeckState),
|
||||
DeckSubWeapons: make(map[string][]string),
|
||||
DeckParts: make(map[string][]string),
|
||||
Quests: make(map[int32]store.UserQuestState),
|
||||
QuestMissions: make(map[store.QuestMissionKey]store.UserQuestMissionState),
|
||||
SideStoryQuests: make(map[int32]store.SideStoryQuestProgress),
|
||||
QuestLimitContentStatus: make(map[int32]store.QuestLimitContentStatus),
|
||||
BigHuntMaxScores: make(map[int32]store.BigHuntMaxScore),
|
||||
BigHuntStatuses: make(map[int32]store.BigHuntStatus),
|
||||
BigHuntScheduleMaxScores: make(map[store.BigHuntScheduleScoreKey]store.BigHuntScheduleMaxScore),
|
||||
BigHuntWeeklyMaxScores: make(map[store.BigHuntWeeklyScoreKey]store.BigHuntWeeklyMaxScore),
|
||||
BigHuntWeeklyStatuses: make(map[int64]store.BigHuntWeeklyStatus),
|
||||
WeaponStories: make(map[int32]store.WeaponStoryState),
|
||||
Missions: map[int32]store.UserMissionState{
|
||||
starterMissionId: {
|
||||
MissionId: starterMissionId,
|
||||
StartDatetime: nowMillis,
|
||||
MissionProgressStatusType: missionInProgress,
|
||||
},
|
||||
},
|
||||
Gimmick: store.GimmickState{
|
||||
Progress: make(map[store.GimmickKey]store.GimmickProgressState),
|
||||
OrnamentProgress: make(map[store.GimmickOrnamentKey]store.GimmickOrnamentProgressState),
|
||||
Sequences: make(map[store.GimmickSequenceKey]store.GimmickSequenceState),
|
||||
Unlocks: make(map[store.GimmickKey]store.GimmickUnlockState),
|
||||
},
|
||||
CageOrnamentRewards: make(map[int32]store.CageOrnamentRewardState),
|
||||
ConsumableItems: make(map[int32]int32),
|
||||
Materials: make(map[int32]int32),
|
||||
Thoughts: make(map[string]store.ThoughtState),
|
||||
Parts: make(map[string]store.PartsState),
|
||||
PartsGroupNotes: make(map[int32]store.PartsGroupNoteState),
|
||||
PartsPresets: make(map[int32]store.PartsPresetState),
|
||||
ImportantItems: make(map[int32]int32),
|
||||
CostumeActiveSkills: make(map[string]store.CostumeActiveSkillState),
|
||||
WeaponSkills: make(map[string][]store.WeaponSkillState),
|
||||
WeaponAbilities: make(map[string][]store.WeaponAbilityState),
|
||||
DeckTypeNotes: make(map[model.DeckType]store.DeckTypeNoteState),
|
||||
WeaponNotes: make(map[int32]store.WeaponNoteState),
|
||||
NaviCutInPlayed: make(map[int32]bool),
|
||||
ViewedMovies: make(map[int32]int64),
|
||||
ContentsStories: make(map[int32]int64),
|
||||
DrawnOmikuji: make(map[int32]int64),
|
||||
PremiumItems: make(map[int32]int64),
|
||||
DokanConfirmed: make(map[int32]bool),
|
||||
ShopItems: make(map[int32]store.UserShopItemState),
|
||||
ShopReplaceableLineup: make(map[int32]store.UserShopReplaceableLineupState),
|
||||
ExploreScores: make(map[int32]store.ExploreScoreState),
|
||||
|
||||
CharacterBoards: make(map[int32]store.CharacterBoardState),
|
||||
CharacterBoardAbilities: make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState),
|
||||
CharacterBoardStatusUps: make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState),
|
||||
|
||||
CostumeAwakenStatusUps: make(map[store.CostumeAwakenStatusKey]store.CostumeAwakenStatusUpState),
|
||||
AutoSaleSettings: make(map[int32]store.AutoSaleSettingState),
|
||||
CharacterRebirths: make(map[int32]store.CharacterRebirthState),
|
||||
}
|
||||
store.EnsureDefaultDeck(user, nowMillis)
|
||||
if grantStarterItems {
|
||||
applyStarterItems(user)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func applyStarterItems(user *store.UserState) {
|
||||
for _, item := range defaultStarterItems {
|
||||
switch item.Type {
|
||||
case model.PossessionTypeFreeGem:
|
||||
user.Gem.FreeGem += item.Qty
|
||||
case model.PossessionTypeConsumableItem:
|
||||
user.ConsumableItems[item.Id] += item.Qty
|
||||
case model.PossessionTypeMaterial:
|
||||
user.Materials[item.Id] += item.Qty
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user