lawless/server/modules/sect.go

502 行
15 KiB
Go

// 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(&sectName, &sectLevel, &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(&sectLevel)
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 "普通"
}
}