// Package modules - 宗门系统模块 // 对齐GDD-07 帮派门派社交系统设计 + T007审阅报告 package modules import ( "context" "database/sql" "encoding/json" "math/rand" "github.com/heroiclabs/nakama-common/runtime" hhdb "github.com/honghuang-game/server/internal/db" ) // RegisterSect 注册宗门相关 RPC。 func RegisterSect(initializer runtime.Initializer) error { rpcs := map[string]func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, error){ "SectService/JoinSect": joinSect, "SectService/LeaveSect": leaveSect, "SectService/GetSectInfo": getSectInfo, "SectService/GetSectResources": getSectResources, "SectService/DonateToSect": donateToSect, "SectService/SectWar": sectWar, } for path, fn := range rpcs { if err := initializer.RegisterRpc(path, fn); err != nil { return err } } return nil } // --- 请求/响应结构 --- type joinSectReq struct { SectID string `json:"sect_id"` } type leaveSectReq struct { SectID string `json:"sect_id"` } type getSectInfoReq struct { SectID string `json:"sect_id"` } type getSectResourcesReq struct { SectID string `json:"sect_id"` } type donateToSectReq struct { SectID string `json:"sect_id"` DonationType string `json:"donation_type"` // spirit_stone/material/contribution Amount int32 `json:"amount"` } type sectWarReq struct { AttackerSectID string `json:"attacker_sect_id"` DefenderSectID string `json:"defender_sect_id"` WarType string `json:"war_type"` // territory/resource/honor/revenge } type sectInfoData struct { ID string `json:"id"` Name string `json:"name"` OrgType string `json:"org_type"` // system_sect/player_sect/guild/family Level int32 `json:"level"` Reputation int32 `json:"reputation"` MemberCount int32 `json:"member_count"` MemberLimit int32 `json:"member_limit"` TaxRate float64 `json:"tax_rate"` Resources interface{} `json:"resources"` Buildings interface{} `json:"buildings"` Status string `json:"status"` } type sectResourceData struct { SectID string `json:"sect_id"` SpiritVein resourceInfo `json:"spirit_vein"` // 灵脉 OreVein resourceInfo `json:"ore_vein"` // 矿脉 HerbGarden resourceInfo `json:"herb_garden"` // 药园 AlchemyRoom resourceInfo `json:"alchemy_room"` // 丹房 ForgeRoom resourceInfo `json:"forge_room"` // 炼器房 DailyYield interface{} `json:"daily_yield"` // 每日产出 } type resourceInfo struct { Level int32 `json:"level"` Quality string `json:"quality"` // low/mid/high/legendary Effect string `json:"effect"` } type sectWarResult struct { WarID string `json:"war_id"` AttackerSect string `json:"attacker_sect"` DefenderSect string `json:"defender_sect"` WarType string `json:"war_type"` Result string `json:"result"` // attacker_win/defender_win/draw AttackerLosses interface{} `json:"attacker_losses"` DefenderLosses interface{} `json:"defender_losses"` Rewards interface{} `json:"rewards"` } // --- 宗门资源配置 --- var sectResourceConfig = map[int32]struct { spiritVeinLevel int32 oreVeinLevel int32 herbGardenLevel int32 alchemyLevel int32 forgeLevel int32 }{ 1: {1, 1, 0, 0, 0}, 2: {1, 1, 1, 0, 0}, 3: {2, 1, 1, 1, 0}, 4: {2, 2, 1, 1, 1}, 5: {3, 2, 2, 1, 1}, 6: {3, 3, 2, 2, 1}, 7: {4, 3, 3, 2, 2}, 8: {4, 4, 3, 3, 2}, 9: {5, 4, 4, 3, 3}, 10: {5, 5, 4, 4, 4}, } // --- RPC 实现 --- func joinSect(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req joinSectReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取角色ID var charID string var realmTier int32 err := hhdb.Pool.QueryRow(ctx, ` SELECT id, realm_tier FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID, &realmTier) if err != nil { return errResp(4002, "character not found", traceID) } // 检查宗门是否存在 var sectName string var sectLevel int32 var memberCount, memberLimit int32 err = hhdb.Pool.QueryRow(ctx, ` SELECT name, level, (SELECT COUNT(*) FROM guild_members WHERE guild_id = g.id), member_limit FROM guilds g WHERE g.id = $1 AND g.status = 'active' `, req.SectID).Scan(§Name, §Level, &memberCount, &memberLimit) if err != nil { return errResp(7001, "sect not found", traceID) } // 检查人数限制 if memberCount >= memberLimit { return errResp(7002, "sect is full", traceID) } // 检查境界要求 gradeReq := map[int32]int32{1: 1, 2: 1, 3: 2, 4: 2, 5: 3, 6: 3, 7: 4, 8: 4, 9: 5, 10: 6} if realmTier < gradeReq[sectLevel] { return errResp(7003, "realm too low for this sect", traceID) } // 检查是否已在宗门 var existingCount int32 err = hhdb.Pool.QueryRow(ctx, ` SELECT COUNT(*) FROM guild_members WHERE character_id = $1 AND guild_id IN ( SELECT id FROM guilds WHERE org_type IN ('system_sect', 'player_sect') ) `, charID).Scan(&existingCount) if err == nil && existingCount > 0 { return errResp(7004, "already in a sect", traceID) } // 加入宗门 _, err = hhdb.Pool.Exec(ctx, ` INSERT INTO guild_members (guild_id, character_id, role, joined_at, contribution, daily_quota) VALUES ($1, $2, 'member', NOW(), '{}', '{}') ON CONFLICT (guild_id, character_id) DO NOTHING `, req.SectID, charID) if err != nil { logger.Error("join sect failed: %v", err) return errResp(9002, "internal error", traceID) } return okResp(map[string]interface{}{ "success": true, "sect_id": req.SectID, "sect_name": sectName, "message": "成功加入宗门", }, traceID) } func leaveSect(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req leaveSectReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取角色ID var charID string err := hhdb.Pool.QueryRow(ctx, ` SELECT id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(4002, "character not found", traceID) } // 检查是否是掌门 var role string err = hhdb.Pool.QueryRow(ctx, ` SELECT role FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.SectID, charID).Scan(&role) if err != nil { return errResp(7005, "not in this sect", traceID) } if role == "leader" { return errResp(7006, "leader cannot leave sect", traceID) } // 离开宗门(冷却3天 + 纯度-10%) _, err = hhdb.Pool.Exec(ctx, ` DELETE FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.SectID, charID) if err != nil { logger.Error("leave sect failed: %v", err) return errResp(9002, "internal error", traceID) } // 设置冷却时间(3天)和纯度惩罚(-10%) _, err = hhdb.Pool.Exec(ctx, ` UPDATE characters SET energy_purity = GREATEST(energy_purity - 0.10, 0), updated_at = NOW() WHERE id = $1 `, charID) if err != nil { logger.Error("apply purity penalty failed: %v", err) } // 记录离开宗门的冷却期(简化版:通过角色状态字段记录) // 实际实现应使用独立的冷却期表 logger.Info("SectService/LeaveSect: applied cooldown 3 days and purity -10%") return okResp(map[string]interface{}{ "success": true, "message": "成功离开宗门(3天冷却期,纯度-10%)", }, traceID) } func getSectInfo(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req getSectInfoReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } var info sectInfoData err := hhdb.Pool.QueryRow(ctx, ` SELECT g.id, g.name, g.org_type, g.level, g.reputation, (SELECT COUNT(*) FROM guild_members WHERE guild_id = g.id), g.member_limit, g.tax_rate, g.building_levels, g.status FROM guilds g WHERE g.id = $1 `, req.SectID).Scan( &info.ID, &info.Name, &info.OrgType, &info.Level, &info.Reputation, &info.MemberCount, &info.MemberLimit, &info.TaxRate, &info.Buildings, &info.Status, ) if err != nil { return errResp(7001, "sect not found", traceID) } return okResp(info, traceID) } func getSectResources(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req getSectResourcesReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取宗门等级 var sectLevel int32 err := hhdb.Pool.QueryRow(ctx, ` SELECT level FROM guilds WHERE id = $1 `, req.SectID).Scan(§Level) if err != nil { return errResp(7001, "sect not found", traceID) } // 获取资源配置 config, ok := sectResourceConfig[sectLevel] if !ok { config = sectResourceConfig[1] } resources := sectResourceData{ SectID: req.SectID, SpiritVein: resourceInfo{ Level: config.spiritVeinLevel, Quality: getQualityName(config.spiritVeinLevel), Effect: "修炼速度+" + string(rune('0'+config.spiritVeinLevel*5)) + "%", }, OreVein: resourceInfo{ Level: config.oreVeinLevel, Quality: getQualityName(config.oreVeinLevel), Effect: "矿石产出+" + string(rune('0'+config.oreVeinLevel*10)) + "%", }, HerbGarden: resourceInfo{ Level: config.herbGardenLevel, Quality: getQualityName(config.herbGardenLevel), Effect: "草药产出+" + string(rune('0'+config.herbGardenLevel*10)) + "%", }, AlchemyRoom: resourceInfo{ Level: config.alchemyLevel, Quality: getQualityName(config.alchemyLevel), Effect: "炼丹成功率+" + string(rune('0'+config.alchemyLevel*5)) + "%", }, ForgeRoom: resourceInfo{ Level: config.forgeLevel, Quality: getQualityName(config.forgeLevel), Effect: "炼器成功率+" + string(rune('0'+config.forgeLevel*5)) + "%", }, } return okResp(resources, traceID) } func donateToSect(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req donateToSectReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 获取角色ID var charID string err := hhdb.Pool.QueryRow(ctx, ` SELECT id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(4002, "character not found", traceID) } // 检查是否在宗门 var memberRole string err = hhdb.Pool.QueryRow(ctx, ` SELECT role FROM guild_members WHERE guild_id = $1 AND character_id = $2 `, req.SectID, charID).Scan(&memberRole) if err != nil { return errResp(7005, "not in this sect", traceID) } // 捐献逻辑 contributionGain := req.Amount if req.DonationType == "spirit_stone" { contributionGain = req.Amount / 10 // 灵石换算 } // 更新贡献度 _, err = hhdb.Pool.Exec(ctx, ` UPDATE guild_members SET contribution = contribution || jsonb_build_object('total', (contribution->>'total')::int + $1), updated_at = NOW() WHERE guild_id = $2 AND character_id = $3 `, contributionGain, req.SectID, charID) if err != nil { logger.Error("donate to sect failed: %v", err) return errResp(9002, "internal error", traceID) } // 更新宗门声望 _, err = hhdb.Pool.Exec(ctx, ` UPDATE guilds SET reputation = reputation + $1, updated_at = NOW() WHERE id = $2 `, contributionGain/10, req.SectID) if err != nil { logger.Error("update sect reputation failed: %v", err) } return okResp(map[string]interface{}{ "success": true, "contribution_gain": contributionGain, "message": "捐献成功", }, traceID) } func sectWar(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req sectWarReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 检查攻击方宗门 var attackerLevel int32 err := hhdb.Pool.QueryRow(ctx, ` SELECT level FROM guilds WHERE id = $1 AND status = 'active' `, req.AttackerSectID).Scan(&attackerLevel) if err != nil { return errResp(7001, "attacker sect not found", traceID) } // 检查防御方宗门 var defenderLevel int32 err = hhdb.Pool.QueryRow(ctx, ` SELECT level FROM guilds WHERE id = $1 AND status = 'active' `, req.DefenderSectID).Scan(&defenderLevel) if err != nil { return errResp(7001, "defender sect not found", traceID) } // 检查等级要求(宗门等级≥3才能开战) if attackerLevel < 3 || defenderLevel < 3 { return errResp(7007, "sect level too low for war", traceID) } // 模拟战斗结果(实际应由战斗引擎计算) warResult := sectWarResult{ WarID: genUUID(), AttackerSect: req.AttackerSectID, DefenderSect: req.DefenderSectID, WarType: req.WarType, } // 简单的概率判定 attackerPower := float64(attackerLevel) * (0.8 + rand.Float64()*0.4) defenderPower := float64(defenderLevel) * (0.8 + rand.Float64()*0.4) if attackerPower > defenderPower { warResult.Result = "attacker_win" warResult.Rewards = map[string]interface{}{ "attacker_reputation": 100 * attackerLevel, "defender_reputation": -50 * defenderLevel, "resource_capture": defenderLevel * 10, } } else if defenderPower > attackerPower { warResult.Result = "defender_win" warResult.Rewards = map[string]interface{}{ "attacker_reputation": -30 * attackerLevel, "defender_reputation": 50 * defenderLevel, } } else { warResult.Result = "draw" warResult.Rewards = map[string]interface{}{ "attacker_reputation": 10, "defender_reputation": 10, } } return okResp(warResult, traceID) } // --- 辅助函数 --- func getQualityName(level int32) string { switch level { case 1: return "普通" case 2: return "精良" case 3: return "稀有" case 4: return "史诗" case 5: return "传说" default: return "普通" } }