Add consumable item sell functionality

This commit is contained in:
Ilya Groshev
2026-04-16 17:22:57 +03:00
parent 409f1f8a11
commit 9453743964
6 changed files with 128 additions and 11 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# 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_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/consumableitem.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
+4
View File
@@ -55,6 +55,7 @@ func startGRPC(
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
@@ -90,6 +91,7 @@ func startGRPC(
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
@@ -126,6 +128,7 @@ func registerServices(
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
@@ -163,6 +166,7 @@ func registerServices(
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.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, 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))
+7
View File
@@ -120,6 +120,12 @@ func main() {
}
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
if err != nil {
log.Fatalf("load consumable item catalog: %v", err)
}
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
if err != nil {
log.Fatalf("load costume catalog: %v", err)
@@ -184,6 +190,7 @@ func main() {
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
@@ -0,0 +1,31 @@
package masterdata
import (
"fmt"
"lunar-tear/server/internal/utils"
)
type ConsumableItemRow struct {
ConsumableItemId int32 `json:"ConsumableItemId"`
SellPrice int32 `json:"SellPrice"`
}
type ConsumableItemCatalog struct {
All map[int32]ConsumableItemRow
}
func LoadConsumableItemCatalog() (*ConsumableItemCatalog, error) {
rows, err := utils.ReadJSON[ConsumableItemRow]("EntityMConsumableItemTable.json")
if err != nil {
return nil, fmt.Errorf("load consumable item table: %w", err)
}
catalog := &ConsumableItemCatalog{
All: make(map[int32]ConsumableItemRow, len(rows)),
}
for _, row := range rows {
catalog.All[row.ConsumableItemId] = row
}
return catalog, nil
}
+75
View File
@@ -0,0 +1,75 @@
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"
)
type ConsumableItemServiceServer struct {
pb.UnimplementedConsumableItemServiceServer
users store.UserRepository
sessions store.SessionRepository
catalog *masterdata.ConsumableItemCatalog
config *masterdata.GameConfig
}
func NewConsumableItemServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ConsumableItemCatalog, config *masterdata.GameConfig) *ConsumableItemServiceServer {
return &ConsumableItemServiceServer{users: users, sessions: sessions, catalog: catalog, config: config}
}
func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.ConsumableItemSellRequest) (*pb.ConsumableItemSellResponse, error) {
log.Printf("[ConsumableItemService] Sell: %d item(s)", len(req.ConsumableItemPossession))
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
totalGold := int32(0)
for _, item := range req.ConsumableItemPossession {
row, ok := s.catalog.All[item.ConsumableItemId]
if !ok {
log.Printf("[ConsumableItemService] Sell: unknown consumableItemId=%d, skipping", item.ConsumableItemId)
continue
}
cur := user.ConsumableItems[item.ConsumableItemId]
if cur < item.Count {
log.Printf("[ConsumableItemService] Sell: insufficient consumableItemId=%d have=%d need=%d", item.ConsumableItemId, cur, item.Count)
continue
}
user.ConsumableItems[item.ConsumableItemId] -= item.Count
if user.ConsumableItems[item.ConsumableItemId] <= 0 {
delete(user.ConsumableItems, item.ConsumableItemId)
}
gold := row.SellPrice * item.Count
totalGold += gold
log.Printf("[ConsumableItemService] Sell: consumableItemId=%d x%d -> %d gold", item.ConsumableItemId, item.Count, gold)
}
if totalGold > 0 {
user.ConsumableItems[s.config.ConsumableItemIdForGold] += totalGold
log.Printf("[ConsumableItemService] Sell: total gold +%d", totalGold)
}
})
if err != nil {
return nil, fmt.Errorf("consumable item sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), []string{"IUserConsumableItem"})
diff := tracker.Apply(snapshot, tables)
return &pb.ConsumableItemSellResponse{
DiffUserData: diff,
}, nil
}
+9 -9
View File
@@ -6,29 +6,29 @@ import "proto/data.proto";
package apb.api.consumableitem;
service ConsumableitemService {
rpc UseEffectItem (UseEffectItemRequest) returns (UseEffectItemResponse);
rpc Sell (SellRequest) returns (SellResponse);
service ConsumableItemService {
rpc UseEffectItem (ConsumableItemUseEffectItemRequest) returns (ConsumableItemUseEffectItemResponse);
rpc Sell (ConsumableItemSellRequest) returns (ConsumableItemSellResponse);
}
message UseEffectItemRequest {
message ConsumableItemUseEffectItemRequest {
int32 consumableItemId = 1;
int32 count = 2;
}
message UseEffectItemResponse {
message ConsumableItemUseEffectItemResponse {
map<string, apb.api.data.DiffData> diffUserData = 99;
}
message SellRequest {
repeated SellPossession consumableItemPossession = 1;
message ConsumableItemSellRequest {
repeated ConsumableItemSellPossession consumableItemPossession = 1;
}
message SellPossession {
message ConsumableItemSellPossession {
int32 consumableItemId = 1;
int32 count = 2;
}
message SellResponse {
message ConsumableItemSellResponse {
map<string, apb.api.data.DiffData> diffUserData = 99;
}