lawless/server/modules/character.go

218 行
6.6 KiB
Go

package modules
import (
"context"
"database/sql"
"encoding/json"
"github.com/heroiclabs/nakama-common/runtime"
)
// RegisterCharacter 注册角色相关 RPC。
func RegisterCharacter(initializer runtime.Initializer) error {
if err := initializer.RegisterRpc("CharacterService/CreateCharacter", createCharacter); err != nil {
return err
}
if err := initializer.RegisterRpc("CharacterService/GetCharacter", getCharacter); err != nil {
return err
}
return nil
}
type createCharacterReq struct {
Name string `json:"name"`
RaceID string `json:"race_id"`
BirthWorldTier int32 `json:"birth_world_tier"`
}
type getCharacterReq struct {
CharacterID string `json:"character_id"`
WithSnapshot bool `json:"with_snapshot"`
}
type characterData struct {
CharacterID string `json:"character_id"`
PlayerID string `json:"player_id"`
Name string `json:"name"`
RaceID string `json:"race_id"`
WorldTier int32 `json:"world_tier"`
RealmTier int32 `json:"realm_tier"`
MinorRealm int32 `json:"minor_realm"`
RealmStatus string `json:"realm_status"`
Level int32 `json:"level"`
Exp int64 `json:"exp"`
Status string `json:"status"`
BaseStats string `json:"base_stats"`
BattleStats string `json:"battle_stats"`
SanCurrent int32 `json:"san_current"`
SanMax int32 `json:"san_max"`
CrimeScore int32 `json:"crime_score"`
HeavenlyValue int32 `json:"heavenly_value"`
KarmaValue int32 `json:"karma_value"`
ReputationScore int32 `json:"reputation_score"`
LastOnlineAt string `json:"last_online_at"`
CreatedAt string `json:"created_at"`
}
func createCharacter(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 createCharacterReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 校验参数
if req.Name == "" || len(req.Name) > 32 {
return errResp(2002, "invalid name length", traceID)
}
if req.RaceID == "" {
return errResp(2003, "race_id required", traceID)
}
if req.BirthWorldTier < 1 || req.BirthWorldTier > 5 {
req.BirthWorldTier = 1 // 默认第1层
}
// 检查种族是否可创建
var canCreate bool
err := db.QueryRowContext(ctx, `
SELECT can_create FROM race_templates WHERE id = $1
`, req.RaceID).Scan(&canCreate)
if err != nil {
return errResp(2004, "invalid race", traceID)
}
if !canCreate {
return errResp(2005, "race not creatable", traceID)
}
// 检查角色名是否已存在
var nameCount int32
err = db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM characters WHERE name = $1
`, req.Name).Scan(&nameCount)
if err == nil && nameCount > 0 {
return errResp(2006, "name already exists", traceID)
}
// 获取玩家ID
var playerID string
err = db.QueryRowContext(ctx, `
SELECT id FROM players WHERE nakama_user_id = $1
`, uid).Scan(&playerID)
if err != nil {
return errResp(4002, "player not found", traceID)
}
// 获取种族初始属性
var baseStatsJSON string
err = db.QueryRowContext(ctx, `
SELECT COALESCE(description, '{}') FROM race_templates WHERE id = $1
`, req.RaceID).Scan(&baseStatsJSON)
if err != nil {
baseStatsJSON = '{"str":10,"vit":10,"wis":10,"agi":10,"spi":10,"luk":10}'
}
// 创建角色
var charID string
err = db.QueryRowContext(ctx, `
INSERT INTO characters (
player_id, name, race_id, birth_race_id, birth_world_tier,
world_tier, realm_tier, minor_realm, realm_status,
level, exp, status, base_stats, battle_stats,
san_current, san_max, crime_score, heavenly_value, karma_value,
reputation_score, last_online_at, created_at, updated_at
) VALUES (
$1, $2, $3, $3, $4,
$4, 1, 1, 'normal',
1, 0, 'active', $5, '{}',
100, 100, 0, 0, 0,
0, NOW(), NOW(), NOW()
)
RETURNING id::text
`, playerID, req.Name, req.RaceID, req.BirthWorldTier, baseStatsJSON).Scan(&charID)
if err != nil {
logger.Error("create character failed: %v", err)
return errResp(9002, "internal error", traceID)
}
// 创建种族状态记录
_, err = db.ExecContext(ctx, `
INSERT INTO character_race_states (character_id, main_race_id, bloodline_data, race_talents, hidden_talents)
VALUES ($1, $2, '{}', '{}', '{}')
`, charID, req.RaceID)
if err != nil {
logger.Error("create race state failed: %v", err)
}
// 创建货币余额记录
_, err = db.ExecContext(ctx, `
INSERT INTO currency_balances (character_id, currency_code, amount, total_earned, total_spent)
VALUES ($1, 'copper', 100, 100, 0)
`, charID)
if err != nil {
logger.Error("create currency balance failed: %v", err)
}
logger.Info("CharacterService/CreateCharacter success: char_id=%s name=%s race=%s", charID, req.Name, req.RaceID)
return okResp(characterData{
CharacterID: charID,
PlayerID: playerID,
Name: req.Name,
RaceID: req.RaceID,
WorldTier: req.BirthWorldTier,
RealmTier: 1,
MinorRealm: 1,
Level: 1,
Exp: 0,
Status: "active",
}, traceID)
}
func getCharacter(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 getCharacterReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2005, "invalid payload", traceID)
}
// 查询玩家ID
var playerID string
err := db.QueryRowContext(ctx, `
SELECT id FROM players WHERE nakama_user_id = $1
`, uid).Scan(&playerID)
if err != nil {
return errResp(4002, "player not found", traceID)
}
// 查询角色
var char characterData
err = db.QueryRowContext(ctx, `
SELECT id, player_id, name, race_id, world_tier, realm_tier, minor_realm,
realm_status, level, exp, status, base_stats, battle_stats,
san_current, san_max, crime_score, heavenly_value, karma_value,
reputation_score, last_online_at, created_at
FROM characters
WHERE id = $1 AND player_id = $2
`, req.CharacterID, playerID).Scan(
&char.CharacterID, &char.PlayerID, &char.Name, &char.RaceID,
&char.WorldTier, &char.RealmTier, &char.MinorRealm,
&char.RealmStatus, &char.Level, &char.Exp, &char.Status,
&char.BaseStats, &char.BattleStats,
&char.SanCurrent, &char.SanMax, &char.CrimeScore,
&char.HeavenlyValue, &char.KarmaValue, &char.ReputationScore,
&char.LastOnlineAt, &char.CreatedAt,
)
if err != nil {
return errResp(2007, "character not found", traceID)
}
return okResp(char, traceID)
}