515 行
15 KiB
Go
515 行
15 KiB
Go
|
|
package modules
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"database/sql"
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"math/rand"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/heroiclabs/nakama-common/runtime"
|
|||
|
|
"github.com/jackc/pgx/v5"
|
|||
|
|
|
|||
|
|
"github.com/honghuang-game/server/internal/battle"
|
|||
|
|
hhdb "github.com/honghuang-game/server/internal/db"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// RegisterBattle 注册战斗相关 RPC。
|
|||
|
|
func RegisterBattle(initializer runtime.Initializer) error {
|
|||
|
|
if err := initializer.RegisterRpc("BattleService/StartCombat", startCombat); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if err := initializer.RegisterRpc("BattleService/GetBattleReport", getBattleReport); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if err := initializer.RegisterRpc("BattleService/PvpChallenge", pvpChallenge); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type startCombatReq struct {
|
|||
|
|
BattleType string `json:"battle_type"`
|
|||
|
|
ContextID string `json:"context_id"`
|
|||
|
|
PartyMembers []string `json:"party_members"`
|
|||
|
|
PreferredSkills []string `json:"preferred_skills"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type pvpChallengeReq struct {
|
|||
|
|
TargetCharacterID string `json:"target_character_id"`
|
|||
|
|
BountyID string `json:"bounty_id"`
|
|||
|
|
UsePaidChance bool `json:"use_paid_chance"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type battleReportReq struct {
|
|||
|
|
BattleID string `json:"battle_id"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type combatData struct {
|
|||
|
|
BattleID string `json:"battle_id"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
ResultSummary interface{} `json:"result_summary"`
|
|||
|
|
Drops interface{} `json:"drops"`
|
|||
|
|
DeathPenaltyApplied bool `json:"death_penalty_applied"`
|
|||
|
|
HonorPoints int32 `json:"honor_points"`
|
|||
|
|
DailyPvpCount int32 `json:"daily_pvp_count"`
|
|||
|
|
DailyPvpLimit int32 `json:"daily_pvp_limit"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type battleReportData struct {
|
|||
|
|
BattleID string `json:"battle_id"`
|
|||
|
|
Type string `json:"type"`
|
|||
|
|
RealmTier int32 `json:"realm_tier"`
|
|||
|
|
GameTimestamp string `json:"game_timestamp"`
|
|||
|
|
Attacker interface{} `json:"attacker"`
|
|||
|
|
Defender interface{} `json:"defender"`
|
|||
|
|
Rounds interface{} `json:"rounds"`
|
|||
|
|
Result interface{} `json:"result"`
|
|||
|
|
SpecialEvents interface{} `json:"special_events"`
|
|||
|
|
Drops interface{} `json:"drops"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
battleTypeExpedition = "expedition_pve"
|
|||
|
|
battleTypeDungeon = "dungeon_pve"
|
|||
|
|
battleTypeRuin = "ruin_pve"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// startCombat 触发一场 PVE 战斗,服务端完整计算并返回战报摘要。
|
|||
|
|
func startCombat(ctx context.Context, logger runtime.Logger, _ *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|||
|
|
traceID := newTraceID()
|
|||
|
|
uid := userIDFromCtx(ctx)
|
|||
|
|
if uid == "" {
|
|||
|
|
return errResp(1001, "missing token", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var req startCombatReq
|
|||
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|||
|
|
return errResp(4001, "invalid payload", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if req.BattleType != battleTypeExpedition && req.BattleType != battleTypeDungeon && req.BattleType != battleTypeRuin {
|
|||
|
|
return errResp(4001, "invalid battle type", traceID)
|
|||
|
|
}
|
|||
|
|
if req.ContextID == "" {
|
|||
|
|
return errResp(4002, "missing context_id", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 取当前玩家激活角色(若多角色则取最近创建)
|
|||
|
|
char, err := loadCharacterForBattle(ctx, uid)
|
|||
|
|
if err != nil {
|
|||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|||
|
|
return errResp(4002, "character not found", traceID)
|
|||
|
|
}
|
|||
|
|
logger.Error("load character failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
skills, err := loadCharacterSkills(ctx, char.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("load skills failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
player := battle.NewFighterFromJSON(
|
|||
|
|
char.ID, char.Name, char.RaceID, "attacker",
|
|||
|
|
int(char.WorldTier), int(char.RealmTier),
|
|||
|
|
char.BaseStats, "neutral", "none", true,
|
|||
|
|
)
|
|||
|
|
for _, s := range skills {
|
|||
|
|
player.AddSkill(s)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
monster := battle.GenerateMonster(int(char.RealmTier), int(char.WorldTier), player, rand.New(rand.NewSource(time.Now().UnixNano())))
|
|||
|
|
|
|||
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|||
|
|
eng := battle.NewEngine(player, monster, rng, req.BattleType, int(char.RealmTier))
|
|||
|
|
report := eng.Run()
|
|||
|
|
|
|||
|
|
battleID, err := persistBattle(ctx, char.ID, req.BattleType, char.WorldTier, char.RealmTier, report)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("persist battle failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
report.BattleID = battleID
|
|||
|
|
|
|||
|
|
if err := applyBattleRewards(ctx, char.ID, char.WorldTier, char.RealmTier, report); err != nil {
|
|||
|
|
logger.Error("apply battle rewards failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return okResp(combatData{
|
|||
|
|
BattleID: battleID,
|
|||
|
|
Status: "completed",
|
|||
|
|
ResultSummary: report.Result,
|
|||
|
|
Drops: report.Drops,
|
|||
|
|
DeathPenaltyApplied: false,
|
|||
|
|
HonorPoints: 0,
|
|||
|
|
}, traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getBattleReport 查询完整文字战报。
|
|||
|
|
func getBattleReport(ctx context.Context, logger runtime.Logger, _ *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|||
|
|
traceID := newTraceID()
|
|||
|
|
uid := userIDFromCtx(ctx)
|
|||
|
|
if uid == "" {
|
|||
|
|
return errResp(1001, "missing token", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var req battleReportReq
|
|||
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|||
|
|
return errResp(4005, "invalid payload", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var raw []byte
|
|||
|
|
err := hhdb.Pool.QueryRow(ctx, `
|
|||
|
|
SELECT bl.report
|
|||
|
|
FROM battle_logs bl
|
|||
|
|
JOIN battles b ON bl.battle_id = b.id
|
|||
|
|
WHERE bl.battle_id = $1 AND (b.attacker_id = (SELECT id FROM characters WHERE player_id = $2 LIMIT 1) OR b.defender_id = (SELECT id FROM characters WHERE player_id = $2 LIMIT 1))
|
|||
|
|
`, req.BattleID, uid).Scan(&raw)
|
|||
|
|
if err != nil {
|
|||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|||
|
|
return errResp(4005, "battle report not found", traceID)
|
|||
|
|
}
|
|||
|
|
logger.Error("get battle report failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var data interface{}
|
|||
|
|
_ = json.Unmarshal(raw, &data)
|
|||
|
|
|
|||
|
|
return okResp(data, traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// pvpChallenge 向同 realm_tier 玩家发起战书(当前版本保留桩,待 PVP 完整规则接入)。
|
|||
|
|
func pvpChallenge(ctx context.Context, logger runtime.Logger, _ *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
|
|||
|
|
traceID := newTraceID()
|
|||
|
|
uid := userIDFromCtx(ctx)
|
|||
|
|
if uid == "" {
|
|||
|
|
return errResp(1001, "missing token", traceID)
|
|||
|
|
}
|
|||
|
|
var req pvpChallengeReq
|
|||
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|||
|
|
return errResp(4007, "invalid payload", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取挑战者信息
|
|||
|
|
attacker, err := loadCharacterForBattle(ctx, uid)
|
|||
|
|
if err != nil {
|
|||
|
|
return errResp(4002, "character not found", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询目标玩家
|
|||
|
|
var defender characterBattleInfo
|
|||
|
|
err = hhdbPool.QueryRow(ctx, `
|
|||
|
|
SELECT id, name, race_id, world_tier, realm_tier, minor_realm, base_stats
|
|||
|
|
FROM characters WHERE id = $1 AND status = 'active'
|
|||
|
|
`, req.TargetCharacterID).Scan(&defender.ID, &defender.Name, &defender.RaceID,
|
|||
|
|
&defender.WorldTier, &defender.RealmTier, &defender.MinorRealm, &defender.BaseStats)
|
|||
|
|
if err != nil {
|
|||
|
|
return errResp(4008, "target not found", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查同 realm_tier
|
|||
|
|
if attacker.RealmTier != defender.RealmTier {
|
|||
|
|
return errResp(4009, "realm tier mismatch", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查每日次数
|
|||
|
|
var dailyCount int32
|
|||
|
|
err = hhdbPool.QueryRow(ctx, `
|
|||
|
|
SELECT COUNT(*) FROM battles
|
|||
|
|
WHERE attacker_id = $1 AND battle_type = 'pvp'
|
|||
|
|
AND created_at >= CURRENT_DATE
|
|||
|
|
`, attacker.ID).Scan(&dailyCount)
|
|||
|
|
if err != nil {
|
|||
|
|
dailyCount = 0
|
|||
|
|
}
|
|||
|
|
if dailyCount >= 20 {
|
|||
|
|
return errResp(4010, "daily pvp limit reached", traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载技能
|
|||
|
|
skills, err := loadCharacterSkills(ctx, attacker.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
skills = []*battle.SkillInstance{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建战斗实体
|
|||
|
|
player := battle.NewFighterFromJSON(
|
|||
|
|
attacker.ID, attacker.Name, attacker.RaceID, "attacker",
|
|||
|
|
int(attacker.WorldTier), int(attacker.RealmTier),
|
|||
|
|
attacker.BaseStats, "neutral", "none", true,
|
|||
|
|
)
|
|||
|
|
for _, s := range skills {
|
|||
|
|
player.AddSkill(s)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建怪物作为PVP对手(简化版)
|
|||
|
|
monster := battle.NewFighterFromJSON(
|
|||
|
|
defender.ID, defender.Name, defender.RaceID, "defender",
|
|||
|
|
int(defender.WorldTier), int(defender.RealmTier),
|
|||
|
|
defender.BaseStats, "neutral", "none", false,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 运行战斗
|
|||
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|||
|
|
eng := battle.NewEngine(player, monster, rng, "pvp", int(attacker.RealmTier))
|
|||
|
|
report := eng.Run()
|
|||
|
|
|
|||
|
|
// 持久化战斗记录
|
|||
|
|
battleID, err := persistBattle(ctx, attacker.ID, "pvp", attacker.WorldTier, attacker.RealmTier, report)
|
|||
|
|
if err != nil {
|
|||
|
|
logger.Error("persist pvp battle failed: %v", err)
|
|||
|
|
return errResp(9002, "internal error", traceID)
|
|||
|
|
}
|
|||
|
|
report.BattleID = battleID
|
|||
|
|
|
|||
|
|
// 应用战斗奖励
|
|||
|
|
if err := applyBattleRewards(ctx, attacker.ID, attacker.WorldTier, attacker.RealmTier, report); err != nil {
|
|||
|
|
logger.Error("apply pvp rewards failed: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.Info("BattleService/PvpChallenge success: battle_id=%s winner=%s", battleID, report.Result.Winner)
|
|||
|
|
return okResp(combatData{
|
|||
|
|
BattleID: battleID,
|
|||
|
|
Status: "completed",
|
|||
|
|
ResultSummary: report.Result,
|
|||
|
|
Drops: report.Drops,
|
|||
|
|
DailyPvpCount: dailyCount + 1,
|
|||
|
|
DailyPvpLimit: 20,
|
|||
|
|
}, traceID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 数据加载与持久化
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
type characterBattleInfo struct {
|
|||
|
|
ID string
|
|||
|
|
Name string
|
|||
|
|
RaceID string
|
|||
|
|
WorldTier int32
|
|||
|
|
RealmTier int32
|
|||
|
|
MinorRealm int32
|
|||
|
|
BaseStats []byte
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func loadCharacterForBattle(ctx context.Context, playerID string) (*characterBattleInfo, error) {
|
|||
|
|
var c characterBattleInfo
|
|||
|
|
err := hhdb.Pool.QueryRow(ctx, `
|
|||
|
|
SELECT id, name, race_id, world_tier, realm_tier, minor_realm, base_stats
|
|||
|
|
FROM characters
|
|||
|
|
WHERE player_id = $1 AND status = 'active'
|
|||
|
|
ORDER BY created_at DESC
|
|||
|
|
LIMIT 1
|
|||
|
|
`, playerID).Scan(&c.ID, &c.Name, &c.RaceID, &c.WorldTier, &c.RealmTier, &c.MinorRealm, &c.BaseStats)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &c, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type skillTemplateData struct {
|
|||
|
|
BaseCoef float64 `json:"base_coef"`
|
|||
|
|
CD int `json:"cd"`
|
|||
|
|
Cost float64 `json:"cost"`
|
|||
|
|
TriggerRate float64 `json:"trigger_rate"`
|
|||
|
|
TargetType string `json:"target_type"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func loadCharacterSkills(ctx context.Context, characterID string) ([]*battle.SkillInstance, error) {
|
|||
|
|
rows, err := hhdb.Pool.Query(ctx, `
|
|||
|
|
SELECT cs.id, cs.custom_name, cs.instance_data,
|
|||
|
|
s.id, s.name, s.element, s.alignment, s.damage_type, s.scaling_attr, s.base_template
|
|||
|
|
FROM character_skills cs
|
|||
|
|
JOIN skills s ON cs.skill_id = s.id
|
|||
|
|
WHERE cs.character_id = $1
|
|||
|
|
`, characterID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer rows.Close()
|
|||
|
|
|
|||
|
|
var skills []*battle.SkillInstance
|
|||
|
|
for rows.Next() {
|
|||
|
|
var instanceID, customName, instanceDataRaw, skillID, name, element, alignment, damageType, scalingAttr, baseTemplateRaw string
|
|||
|
|
if err := rows.Scan(&instanceID, &customName, &instanceDataRaw, &skillID, &name, &element, &alignment, &damageType, &scalingAttr, &baseTemplateRaw); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
baseTmpl := parseSkillTemplate(baseTemplateRaw)
|
|||
|
|
instTmpl := parseSkillTemplate(instanceDataRaw)
|
|||
|
|
|
|||
|
|
// 实例数据覆盖模板默认值
|
|||
|
|
skill := &battle.SkillInstance{
|
|||
|
|
InstanceID: instanceID,
|
|||
|
|
SkillID: skillID,
|
|||
|
|
Name: firstNonEmpty(customName, name),
|
|||
|
|
DamageType: battle.DamageType(damageType),
|
|||
|
|
Element: element,
|
|||
|
|
Alignment: alignment,
|
|||
|
|
ScalingAttr: scalingAttr,
|
|||
|
|
BaseCoef: coalesceFloat(instTmpl.BaseCoef, baseTmpl.BaseCoef, 1.8),
|
|||
|
|
CD: coalesceInt(instTmpl.CD, baseTmpl.CD, 225),
|
|||
|
|
Cost: coalesceFloat(instTmpl.Cost, baseTmpl.Cost, 20),
|
|||
|
|
TriggerRate: coalesceFloat(instTmpl.TriggerRate, baseTmpl.TriggerRate, 1.25),
|
|||
|
|
TargetType: battle.TargetSingle,
|
|||
|
|
}
|
|||
|
|
if instTmpl.TargetType != "" {
|
|||
|
|
skill.TargetType = battle.TargetType(instTmpl.TargetType)
|
|||
|
|
} else if baseTmpl.TargetType != "" {
|
|||
|
|
skill.TargetType = battle.TargetType(baseTmpl.TargetType)
|
|||
|
|
}
|
|||
|
|
if skill.DamageType == "" {
|
|||
|
|
skill.DamageType = battle.DamageTypePhysical
|
|||
|
|
}
|
|||
|
|
skills = append(skills, skill)
|
|||
|
|
}
|
|||
|
|
if err := rows.Err(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 没有任何技能时补一个默认小技能,确保战斗不至于只有普攻
|
|||
|
|
if len(skills) == 0 {
|
|||
|
|
skills = append(skills, &battle.SkillInstance{
|
|||
|
|
InstanceID: "default_small",
|
|||
|
|
SkillID: "default_small",
|
|||
|
|
Name: "基础战技",
|
|||
|
|
DamageType: battle.DamageTypePhysical,
|
|||
|
|
Element: "none",
|
|||
|
|
Alignment: "neutral",
|
|||
|
|
ScalingAttr: "str",
|
|||
|
|
BaseCoef: 1.8,
|
|||
|
|
CD: 225,
|
|||
|
|
Cost: 20,
|
|||
|
|
TriggerRate: 1.25,
|
|||
|
|
TargetType: battle.TargetSingle,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return skills, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseSkillTemplate(raw string) skillTemplateData {
|
|||
|
|
var t skillTemplateData
|
|||
|
|
if raw == "" || raw == "{}" {
|
|||
|
|
return t
|
|||
|
|
}
|
|||
|
|
_ = json.Unmarshal([]byte(raw), &t)
|
|||
|
|
return t
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func firstNonEmpty(a, b string) string {
|
|||
|
|
if a != "" {
|
|||
|
|
return a
|
|||
|
|
}
|
|||
|
|
return b
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func coalesceFloat(values ...float64) float64 {
|
|||
|
|
for _, v := range values {
|
|||
|
|
if v > 0 {
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func coalesceInt(values ...int) int {
|
|||
|
|
for _, v := range values {
|
|||
|
|
if v > 0 {
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// persistBattle 将战报写入 battles / battle_logs 表
|
|||
|
|
func persistBattle(ctx context.Context, characterID, battleType string, worldTier, realmTier int32, report *battle.BattleReport) (string, error) {
|
|||
|
|
reportJSON, err := report.MarshalReport()
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tx, err := hhdb.Pool.Begin(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
defer tx.Rollback(ctx)
|
|||
|
|
|
|||
|
|
var battleID string
|
|||
|
|
err = tx.QueryRow(ctx, `
|
|||
|
|
INSERT INTO battles (battle_type, world_tier, realm_tier, game_timestamp, attacker_id, defender_id, party_a, party_b, status, result_summary)
|
|||
|
|
VALUES ($1, $2, $3, NOW(), $4, NULL, '{}', '{}', 'completed', $5)
|
|||
|
|
RETURNING id::text
|
|||
|
|
`, battleType, worldTier, realmTier, characterID, report.Result).Scan(&battleID)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_, err = tx.Exec(ctx, `
|
|||
|
|
INSERT INTO battle_logs (battle_id, report, special_events, drops)
|
|||
|
|
VALUES ($1, $2, $3, $4)
|
|||
|
|
`, battleID, reportJSON, report.SpecialEvents, report.Drops)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := tx.Commit(ctx); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return battleID, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// applyBattleRewards 发放战斗奖励:货币、经验、审计日志
|
|||
|
|
func applyBattleRewards(ctx context.Context, characterID string, worldTier, realmTier int32, report *battle.BattleReport) error {
|
|||
|
|
if report.Result.Winner != "attacker" || len(report.Drops.Currency) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tx, err := hhdb.Pool.Begin(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer tx.Rollback(ctx)
|
|||
|
|
|
|||
|
|
for _, c := range report.Drops.Currency {
|
|||
|
|
if c.Amount <= 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
_, err = tx.Exec(ctx, `
|
|||
|
|
INSERT INTO currency_balances (character_id, currency_code, amount, total_earned, total_spent)
|
|||
|
|
VALUES ($1, $2, $3, $3, 0)
|
|||
|
|
ON CONFLICT (character_id, currency_code)
|
|||
|
|
DO UPDATE SET amount = currency_balances.amount + EXCLUDED.amount,
|
|||
|
|
total_earned = currency_balances.total_earned + EXCLUDED.amount,
|
|||
|
|
updated_at = NOW()
|
|||
|
|
`, characterID, c.Type, c.Amount)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_, err = tx.Exec(ctx, `
|
|||
|
|
INSERT INTO economy_audit_logs (character_id, entity_type, entity_id, currency_code, flow_type, reason_code, amount, related_id, world_tier)
|
|||
|
|
VALUES ($1, 'character', $1, $2, 'faucet', 'combat_drop', $3, $4, $5)
|
|||
|
|
`, characterID, c.Type, c.Amount, report.BattleID, worldTier)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if report.Drops.Exp > 0 {
|
|||
|
|
_, err = tx.Exec(ctx, `
|
|||
|
|
UPDATE characters SET exp = exp + $1, updated_at = NOW() WHERE id = $2
|
|||
|
|
`, report.Drops.Exp, characterID)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 材料掉落仅记录到战报 drops,实际进背包需要物品模板存在,当前版本保留扩展点
|
|||
|
|
return tx.Commit(ctx)
|
|||
|
|
}
|