725 行
19 KiB
Go
725 行
19 KiB
Go
package battle
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"math/rand"
|
||
"sort"
|
||
"time"
|
||
|
||
"github.com/honghuang-game/server/config"
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 配置读取辅助(带本地默认值,避免 Nacos 不可用时崩溃)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func cfgGet(key string) (interface{}, bool) {
|
||
if config.Global == nil {
|
||
return nil, false
|
||
}
|
||
return config.Global.Get(key)
|
||
}
|
||
|
||
func cfgFloat(key string, def float64) float64 {
|
||
if config.Global == nil {
|
||
return def
|
||
}
|
||
return config.Global.GetFloat64(key, def)
|
||
}
|
||
|
||
func cfgInt(key string, def int) int {
|
||
if config.Global == nil {
|
||
return def
|
||
}
|
||
return config.Global.GetInt(key, def)
|
||
}
|
||
|
||
func toFloat(v interface{}, def float64) float64 {
|
||
if v == nil {
|
||
return def
|
||
}
|
||
switch n := v.(type) {
|
||
case float64:
|
||
return n
|
||
case float32:
|
||
return float64(n)
|
||
case int:
|
||
return float64(n)
|
||
case int64:
|
||
return float64(n)
|
||
case string:
|
||
var f float64
|
||
if _, err := fmt.Sscanf(n, "%f", &f); err == nil && f > 0 {
|
||
return f
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 战报数据结构(与 GDD-03 文字战报 JSON Schema 对齐)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type FighterStatsView struct {
|
||
HPMax int `json:"hp_max"`
|
||
ATK int `json:"atk"`
|
||
DEF int `json:"def"`
|
||
Speed int `json:"speed"`
|
||
Spirit int `json:"spirit"`
|
||
Luck int `json:"luck"`
|
||
Blood *int `json:"blood"`
|
||
}
|
||
|
||
type FighterView struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Race string `json:"race"`
|
||
Job string `json:"job"`
|
||
AvatarKey string `json:"avatar_key,omitempty"`
|
||
Stats FighterStatsView `json:"stats"`
|
||
ActiveSkills []string `json:"active_skills"`
|
||
PassiveTalents []string `json:"passive_talents"`
|
||
}
|
||
|
||
type RoundLog struct {
|
||
Round int `json:"round"`
|
||
Tick int `json:"tick"`
|
||
DayPhase string `json:"day_phase"`
|
||
Actor string `json:"actor"`
|
||
SkillID string `json:"skill_id"`
|
||
SkillName string `json:"skill_name"`
|
||
Damage int `json:"damage"`
|
||
DamageType string `json:"damage_type"`
|
||
IsCrit bool `json:"is_crit"`
|
||
IsEvaded bool `json:"is_evaded"`
|
||
HPAfter map[string]int `json:"hp_after"`
|
||
Text string `json:"text"`
|
||
}
|
||
|
||
type BattleResult struct {
|
||
Winner string `json:"winner"`
|
||
EndCondition string `json:"end_condition"`
|
||
FinalHP map[string]int `json:"final_hp"`
|
||
}
|
||
|
||
type CurrencyDrop struct {
|
||
Type string `json:"type"`
|
||
Amount float64 `json:"amount"`
|
||
}
|
||
|
||
type ItemDrop struct {
|
||
ItemID string `json:"item_id"`
|
||
Quantity int `json:"quantity"`
|
||
}
|
||
|
||
type DropResult struct {
|
||
Currency []CurrencyDrop `json:"currency"`
|
||
Items []ItemDrop `json:"items"`
|
||
HonorPoints int `json:"honor_points"`
|
||
Exp int64 `json:"exp"`
|
||
}
|
||
|
||
type SpecialEvent struct {
|
||
Round int `json:"round"`
|
||
Tick int `json:"tick"`
|
||
Type string `json:"type"`
|
||
Actor string `json:"actor,omitempty"`
|
||
Value int `json:"value,omitempty"`
|
||
Text string `json:"text,omitempty"`
|
||
}
|
||
|
||
type BattleReport struct {
|
||
BattleID string `json:"battle_id"`
|
||
Type string `json:"type"`
|
||
RealmTier int `json:"realm_tier"`
|
||
GameTimestamp string `json:"game_timestamp"`
|
||
RealTimestamp string `json:"real_timestamp"`
|
||
Attacker FighterView `json:"attacker"`
|
||
Defender FighterView `json:"defender"`
|
||
Rounds []RoundLog `json:"rounds"`
|
||
Result BattleResult `json:"result"`
|
||
SpecialEvents []SpecialEvent `json:"special_events"`
|
||
Drops DropResult `json:"drops"`
|
||
DeathPenaltyApplied bool `json:"death_penalty_applied"`
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ATB 战斗引擎
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Engine 负责按行动速度制(ATB)完整模拟一场战斗
|
||
type Engine struct {
|
||
attacker *Fighter
|
||
defender *Fighter
|
||
|
||
battleType string
|
||
realmTier int
|
||
rng *rand.Rand
|
||
|
||
tick int
|
||
roundSeq int
|
||
maxTicks int
|
||
maxActions int
|
||
gaugeMax float64
|
||
baseCoef float64
|
||
|
||
logs []RoundLog
|
||
specialEvents []SpecialEvent
|
||
}
|
||
|
||
// NewEngine 创建战斗引擎
|
||
func NewEngine(attacker, defender *Fighter, rng *rand.Rand, battleType string, realmTier int) *Engine {
|
||
if rng == nil {
|
||
rng = rand.New(rand.NewSource(1))
|
||
}
|
||
return &Engine{
|
||
attacker: attacker,
|
||
defender: defender,
|
||
battleType: battleType,
|
||
realmTier: realmTier,
|
||
rng: rng,
|
||
maxTicks: cfgInt("combat.battle.max_ticks", 3000),
|
||
maxActions: cfgInt("combat.battle.max_actions", 50),
|
||
gaugeMax: cfgFloat("combat.atb.gauge_max", 100),
|
||
baseCoef: cfgFloat("combat.atb.base_coefficient", 0.1),
|
||
}
|
||
}
|
||
|
||
// Run 运行完整战斗模拟并返回文字战报
|
||
func (e *Engine) Run() *BattleReport {
|
||
if e.attacker == nil || e.defender == nil {
|
||
return e.emptyReport("invalid_combatants")
|
||
}
|
||
|
||
for e.tick < e.maxTicks {
|
||
e.tick++
|
||
e.globalTickUpdate()
|
||
|
||
actor := e.pickActor()
|
||
if actor == nil {
|
||
continue
|
||
}
|
||
|
||
e.processAction(actor)
|
||
|
||
if ended, winner, reason := e.checkEnd(); ended {
|
||
return e.buildReport(winner, reason)
|
||
}
|
||
}
|
||
|
||
// 达到最大 ticks,按剩余 HP 百分比判定胜负(PVE 默认攻击方占优)
|
||
winner, reason := e.resolveTimeout()
|
||
return e.buildReport(winner, reason)
|
||
}
|
||
|
||
// globalTickUpdate 每 tick 更新:内力回复、CD 流逝、行动条填充
|
||
func (e *Engine) globalTickUpdate() {
|
||
for _, f := range []*Fighter{e.attacker, e.defender} {
|
||
if !f.IsAlive() {
|
||
continue
|
||
}
|
||
f.RegenMana()
|
||
f.ReduceCooldowns()
|
||
|
||
inc := f.EffectiveSpeed() * e.baseCoef
|
||
f.Gauge += inc
|
||
}
|
||
}
|
||
|
||
// pickActor 选出当前行动条满的单位;多单位同满时按速度+随机扰动排序
|
||
func (e *Engine) pickActor() *Fighter {
|
||
candidates := make([]*Fighter, 0, 2)
|
||
for _, f := range []*Fighter{e.attacker, e.defender} {
|
||
if f.IsAlive() && f.Gauge >= e.gaugeMax {
|
||
candidates = append(candidates, f)
|
||
}
|
||
}
|
||
if len(candidates) == 0 {
|
||
return nil
|
||
}
|
||
|
||
sort.Slice(candidates, func(i, j int) bool {
|
||
vi := candidates[i].EffectiveSpeed() * (0.9 + e.rng.Float64()*0.2)
|
||
vj := candidates[j].EffectiveSpeed() * (0.9 + e.rng.Float64()*0.2)
|
||
return vi > vj
|
||
})
|
||
return candidates[0]
|
||
}
|
||
|
||
// processAction 处理一次行动
|
||
func (e *Engine) processAction(actor *Fighter) {
|
||
target := e.opponent(actor)
|
||
if target == nil || !target.IsAlive() {
|
||
return
|
||
}
|
||
|
||
e.roundSeq++
|
||
|
||
// 眩晕跳过
|
||
if actor.Stunned {
|
||
actor.Stunned = false
|
||
actor.StunRemaining = 0
|
||
actor.ResetGauge()
|
||
e.logRound(actor, nil, 0, false, false, "skip")
|
||
return
|
||
}
|
||
|
||
normal := DefaultNormalAttack(actor)
|
||
skill := SelectSkill(actor, target, normal, e.rng)
|
||
|
||
// 进入 CD / 消耗内力
|
||
if !skill.IsNormal() {
|
||
actor.AddCooldown(skill)
|
||
actor.SpendMana(skill.Cost)
|
||
}
|
||
|
||
// 命中判定
|
||
evaded := false
|
||
if !skill.IsNormal() || skill.DamageType != DamageTypeTrue {
|
||
// 真实伤害普攻也允许被闪避(设计可选),这里统一判定
|
||
hitBonus := 0.0
|
||
if e.rng.Float64() < (target.EvadeRate - hitBonus) {
|
||
evaded = true
|
||
}
|
||
}
|
||
|
||
damage := 0.0
|
||
isCrit := false
|
||
if !evaded {
|
||
damage, isCrit = e.calcDamage(actor, target, skill)
|
||
target.TakeDamage(damage)
|
||
}
|
||
|
||
// 行动后额外内力回复
|
||
actor.ManaRegenAfterAction()
|
||
actor.ActionCount++
|
||
actor.ResetGauge()
|
||
|
||
e.logRound(actor, skill, damage, isCrit, evaded, string(skill.DamageType))
|
||
|
||
if isCrit {
|
||
e.specialEvents = append(e.specialEvents, SpecialEvent{
|
||
Round: e.roundSeq,
|
||
Tick: e.tick,
|
||
Type: "crit",
|
||
Actor: actor.Side,
|
||
Value: int(damage),
|
||
})
|
||
}
|
||
}
|
||
|
||
// calcDamage 按 GDD-03 公式计算最终伤害
|
||
func (e *Engine) calcDamage(attacker, defender *Fighter, skill *SkillInstance) (float64, bool) {
|
||
scalingAttr := skill.ScalingAttr
|
||
if scalingAttr == "" {
|
||
switch skill.DamageType {
|
||
case DamageTypeMagical:
|
||
scalingAttr = "spi"
|
||
case DamageTypeTrue:
|
||
scalingAttr = "str"
|
||
default:
|
||
scalingAttr = "str"
|
||
}
|
||
}
|
||
|
||
var baseAtk float64
|
||
switch scalingAttr {
|
||
case "vit":
|
||
baseAtk = attacker.Base.Vit
|
||
case "wis":
|
||
baseAtk = attacker.Base.Wis
|
||
case "agi":
|
||
baseAtk = attacker.Base.Agi
|
||
case "spi":
|
||
baseAtk = attacker.Base.Spi
|
||
case "luk":
|
||
baseAtk = attacker.Base.Luk
|
||
case "blood":
|
||
baseAtk = attacker.Base.Blood
|
||
default:
|
||
baseAtk = attacker.Base.Str
|
||
}
|
||
|
||
coef := skill.BaseCoef
|
||
if coef <= 0 {
|
||
coef = cfgFloat("combat.skill.normal_attack_coef", 1.0)
|
||
}
|
||
|
||
// 亲和 / 共鸣占位(未接入 GDD-15 时视为 0)
|
||
affinity := 0.0
|
||
resonance := 0.0
|
||
coef *= (1 + affinity) * (1 + resonance)
|
||
|
||
damage := baseAtk * coef
|
||
|
||
// 暴击
|
||
isCrit := false
|
||
critRate := attacker.CritRate
|
||
if critRate > 0 && e.rng.Float64() < critRate {
|
||
multiplier := cfgFloat("combat.crit.multiplier", 1.5)
|
||
damage *= multiplier
|
||
isCrit = true
|
||
}
|
||
|
||
ignoreResist := 0.0
|
||
// 减伤
|
||
switch skill.DamageType {
|
||
case DamageTypePhysical:
|
||
dr := defender.PhysDR - ignoreResist
|
||
if dr < 0 {
|
||
dr = 0
|
||
}
|
||
damage *= (1 - dr)
|
||
case DamageTypeMagical:
|
||
dr := defender.MagDR - ignoreResist
|
||
if dr < 0 {
|
||
dr = 0
|
||
}
|
||
damage *= (1 - dr)
|
||
case DamageTypeTrue:
|
||
// 真实伤害无视物/法减伤
|
||
}
|
||
|
||
// 阵营伤害修正
|
||
factionMod := e.factionModifier(skill.Alignment, defender.Alignment)
|
||
damage *= factionMod
|
||
|
||
// 元素克制修正(物理技能也可带元素标签)
|
||
elemMod, ignore := e.elementModifier(skill.Element, defender.Element)
|
||
damage *= elemMod
|
||
if ignore > 0 {
|
||
// 混沌无视部分抗性已在元素处理中体现
|
||
}
|
||
|
||
if damage < 0 {
|
||
damage = 0
|
||
}
|
||
return damage, isCrit
|
||
}
|
||
|
||
// factionModifier 阵营伤害修正(光明↔暗黑)
|
||
func (e *Engine) factionModifier(skillAlign, targetAlign string) float64 {
|
||
cap := cfgFloat("combat.faction.cap", 0.15)
|
||
if (skillAlign == "light" && targetAlign == "dark") || (skillAlign == "dark" && targetAlign == "light") {
|
||
return 1 + cap
|
||
}
|
||
return 1.0
|
||
}
|
||
|
||
// elementModifier 返回元素克制倍率与无视抗性比例
|
||
func (e *Engine) elementModifier(atkElem, defElem string) (float64, float64) {
|
||
if atkElem == "" || atkElem == "none" {
|
||
return 1.0, 0
|
||
}
|
||
if atkElem == "chaos" {
|
||
// 混沌无视 15% 抗性,不参与克制环
|
||
return 1.0, 0.15
|
||
}
|
||
if advantageOver(atkElem, defElem) {
|
||
return cfgFloat("combat.element.advantage", 1.2), 0
|
||
}
|
||
if disadvantageTo(atkElem, defElem) {
|
||
return cfgFloat("combat.element.disadvantage", 0.85), 0
|
||
}
|
||
return 1.0, 0
|
||
}
|
||
|
||
func advantageOver(atk, def string) bool {
|
||
pairs := map[string][]string{
|
||
"fire": {"nature", "wood", "ice"},
|
||
"water": {"fire"},
|
||
"lightning": {"water", "metal"},
|
||
"earth": {"lightning"},
|
||
"nature": {"earth", "water"},
|
||
"wood": {"earth", "water"},
|
||
"yin": {"yang"},
|
||
"yang": {"yin"},
|
||
}
|
||
for _, d := range pairs[atk] {
|
||
if d == def {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func disadvantageTo(atk, def string) bool {
|
||
pairs := map[string][]string{
|
||
"fire": {"water"},
|
||
"water": {"lightning"},
|
||
"lightning": {"earth"},
|
||
"earth": {"nature", "wood"},
|
||
"nature": {"fire"},
|
||
"wood": {"fire"},
|
||
"yin": {"yang"},
|
||
"yang": {"yin"},
|
||
}
|
||
for _, d := range pairs[atk] {
|
||
if d == def {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// opponent 返回对手
|
||
func (e *Engine) opponent(f *Fighter) *Fighter {
|
||
if f == e.attacker {
|
||
return e.defender
|
||
}
|
||
return e.attacker
|
||
}
|
||
|
||
// checkEnd 检查战斗是否结束
|
||
func (e *Engine) checkEnd() (bool, string, string) {
|
||
if !e.attacker.IsAlive() {
|
||
return true, "defender", "hp_zero"
|
||
}
|
||
if !e.defender.IsAlive() {
|
||
return true, "attacker", "hp_zero"
|
||
}
|
||
if e.attacker.ActionCount >= e.maxActions || e.defender.ActionCount >= e.maxActions {
|
||
winner, _ := e.resolveTimeout()
|
||
return true, winner, "action_count_limit"
|
||
}
|
||
return false, "", ""
|
||
}
|
||
|
||
// resolveTimeout 超时/行动次数上限判胜负:PVE 攻击方 HP% 高则胜;平局攻击方胜
|
||
func (e *Engine) resolveTimeout() (string, string) {
|
||
aPct := e.attacker.HP / e.attacker.HPMax
|
||
dPct := e.defender.HP / e.defender.HPMax
|
||
if aPct >= dPct {
|
||
return "attacker", "action_time_limit"
|
||
}
|
||
return "defender", "action_time_limit"
|
||
}
|
||
|
||
// logRound 写入一条行动日志
|
||
func (e *Engine) logRound(actor *Fighter, skill *SkillInstance, damage float64, isCrit, evaded bool, dmgType string) {
|
||
if skill == nil {
|
||
skill = DefaultNormalAttack(actor)
|
||
}
|
||
text := e.buildText(actor, skill, int(damage), isCrit, evaded)
|
||
hpAfter := map[string]int{
|
||
"attacker": int(e.attacker.HP),
|
||
"defender": int(e.defender.HP),
|
||
}
|
||
e.logs = append(e.logs, RoundLog{
|
||
Round: e.roundSeq,
|
||
Tick: e.tick,
|
||
DayPhase: "day",
|
||
Actor: actor.Side,
|
||
SkillID: skill.SkillID,
|
||
SkillName: skill.Name,
|
||
Damage: int(damage),
|
||
DamageType: dmgType,
|
||
IsCrit: isCrit,
|
||
IsEvaded: evaded,
|
||
HPAfter: hpAfter,
|
||
Text: text,
|
||
})
|
||
}
|
||
|
||
func (e *Engine) buildText(actor *Fighter, skill *SkillInstance, damage int, isCrit, evaded bool) string {
|
||
if evaded {
|
||
return fmt.Sprintf("〔%s〕的 %s 被闪开了!", actor.Name, skill.Name)
|
||
}
|
||
if skill.SkillID == "skip" {
|
||
return fmt.Sprintf("〔%s〕眩晕中无法行动。", actor.Name)
|
||
}
|
||
critText := ""
|
||
if isCrit {
|
||
critText = "暴击!"
|
||
}
|
||
return fmt.Sprintf("〔%s〕施展 %s,造成 %d 点伤害%s", actor.Name, skill.Name, damage, critText)
|
||
}
|
||
|
||
// buildReport 组装最终战报
|
||
func (e *Engine) buildReport(winner, endCondition string) *BattleReport {
|
||
drops := GenerateDrops(e.attacker, e.defender, e.realmTier, e.rng)
|
||
return &BattleReport{
|
||
BattleID: "",
|
||
Type: e.battleType,
|
||
RealmTier: e.realmTier,
|
||
GameTimestamp: time.Now().UTC().Format(time.RFC3339),
|
||
RealTimestamp: time.Now().UTC().Format(time.RFC3339),
|
||
Attacker: fighterView(e.attacker),
|
||
Defender: fighterView(e.defender),
|
||
Rounds: e.logs,
|
||
Result: BattleResult{
|
||
Winner: winner,
|
||
EndCondition: endCondition,
|
||
FinalHP: map[string]int{
|
||
"attacker": int(e.attacker.HP),
|
||
"defender": int(e.defender.HP),
|
||
},
|
||
},
|
||
SpecialEvents: e.specialEvents,
|
||
Drops: drops,
|
||
DeathPenaltyApplied: false,
|
||
}
|
||
}
|
||
|
||
func (e *Engine) emptyReport(reason string) *BattleReport {
|
||
return &BattleReport{
|
||
Type: e.battleType,
|
||
RealmTier: e.realmTier,
|
||
GameTimestamp: time.Now().UTC().Format(time.RFC3339),
|
||
RealTimestamp: time.Now().UTC().Format(time.RFC3339),
|
||
Result: BattleResult{Winner: "draw", EndCondition: reason},
|
||
Drops: DropResult{},
|
||
}
|
||
}
|
||
|
||
func fighterView(f *Fighter) FighterView {
|
||
blood := int(f.Base.Blood)
|
||
var bloodPtr *int
|
||
if f.Base.Blood > 0 {
|
||
bloodPtr = &blood
|
||
}
|
||
return FighterView{
|
||
ID: f.ID,
|
||
Name: f.Name,
|
||
Race: f.RaceID,
|
||
Job: f.Job,
|
||
Stats: FighterStatsView{
|
||
HPMax: int(f.HPMax),
|
||
ATK: int(f.PhysAtk),
|
||
DEF: int(f.PhysDR * 100),
|
||
Speed: int(f.Speed),
|
||
Spirit: int(f.MagAtk),
|
||
Luck: int(f.CritRate * 100),
|
||
Blood: bloodPtr,
|
||
},
|
||
ActiveSkills: f.ActiveSkillIDs(),
|
||
PassiveTalents: []string{},
|
||
}
|
||
}
|
||
|
||
// GenerateMonster 根据玩家强度和境界层级生成一只 PVE 怪物(简化版)
|
||
func GenerateMonster(realmTier, worldTier int, player *Fighter, rng *rand.Rand) *Fighter {
|
||
if rng == nil {
|
||
rng = rand.New(rand.NewSource(1))
|
||
}
|
||
|
||
names := []string{"幽魂", "铁背苍狼", "山魈", "腐沼毒蟾", "赤焰狼妖"}
|
||
races := []string{"ghost", "wolf_yao", "shanxiao", "deep_one", "wolf_yao"}
|
||
elements := []string{"yin", "none", "earth", "yin", "fire"}
|
||
|
||
idx := rng.Intn(len(names))
|
||
name := names[idx]
|
||
race := races[idx]
|
||
element := elements[idx]
|
||
|
||
scale := 0.75 + rng.Float64()*0.15
|
||
base := BaseStats{
|
||
Str: player.Base.Str * scale,
|
||
Vit: player.Base.Vit * scale,
|
||
Wis: player.Base.Wis * scale,
|
||
Agi: player.Base.Agi * scale * (0.8 + rng.Float64()*0.3),
|
||
Spi: player.Base.Spi * scale,
|
||
Luk: player.Base.Luk * scale,
|
||
}
|
||
|
||
monster := NewFighter("npc-"+name, name, race, "defender", worldTier, realmTier, base, "dark", element, false)
|
||
monster.Side = "defender"
|
||
|
||
// 给怪物配置 1~2 个简单技能
|
||
small := &SkillInstance{
|
||
InstanceID: "m_small",
|
||
SkillID: "monster_claw",
|
||
Name: "撕咬",
|
||
DamageType: DamageTypePhysical,
|
||
Element: element,
|
||
Alignment: "dark",
|
||
ScalingAttr: "str",
|
||
BaseCoef: cfgFloat("combat.skill.small_coef_max", 2.5),
|
||
CD: cfgInt("combat.skill.small_cd", 225),
|
||
Cost: cfgFloat("combat.mana.small_skill_cost", 20),
|
||
TriggerRate: cfgFloat("combat.skill.small_trigger_rate", 1.25),
|
||
TargetType: TargetSingle,
|
||
}
|
||
monster.AddSkill(small)
|
||
|
||
if rng.Float64() < 0.4 {
|
||
big := &SkillInstance{
|
||
InstanceID: "m_big",
|
||
SkillID: "monster_frenzy",
|
||
Name: "狂暴扑击",
|
||
DamageType: DamageTypePhysical,
|
||
Element: element,
|
||
Alignment: "dark",
|
||
ScalingAttr: "str",
|
||
BaseCoef: cfgFloat("combat.skill.big_coef_min", 2.5),
|
||
CD: cfgInt("combat.skill.big_cd", 800),
|
||
Cost: cfgFloat("combat.mana.big_skill_cost", 40),
|
||
TriggerRate: cfgFloat("combat.skill.big_trigger_rate", 2.0),
|
||
TargetType: TargetSingle,
|
||
}
|
||
monster.AddSkill(big)
|
||
}
|
||
return monster
|
||
}
|
||
|
||
// GenerateDrops 根据战斗结果生成奖励(货币 + 经验 + 概率材料)
|
||
func GenerateDrops(winner, loser *Fighter, realmTier int, rng *rand.Rand) DropResult {
|
||
if rng == nil {
|
||
rng = rand.New(rand.NewSource(1))
|
||
}
|
||
if winner == nil || !winner.IsPlayer {
|
||
return DropResult{}
|
||
}
|
||
|
||
currencyCode, baseAmount := dropCurrencyForTier(realmTier)
|
||
amount := baseAmount * (0.8 + rng.Float64()*0.4)
|
||
|
||
drop := DropResult{
|
||
Currency: []CurrencyDrop{{Type: currencyCode, Amount: math.Round(amount*100) / 100}},
|
||
Exp: int64(50 * realmTier),
|
||
}
|
||
|
||
// 材料概率掉落
|
||
materialRate := 0.05 + float64(realmTier)*0.03
|
||
if rng.Float64() < materialRate {
|
||
itemID := fmt.Sprintf("monster_material_tier%d", realmTier)
|
||
drop.Items = append(drop.Items, ItemDrop{ItemID: itemID, Quantity: 1})
|
||
}
|
||
return drop
|
||
}
|
||
|
||
func dropCurrencyForTier(realmTier int) (string, float64) {
|
||
defaults := map[int]struct {
|
||
code string
|
||
amount float64
|
||
}{
|
||
1: {"copper", 125},
|
||
2: {"spirit_stone_low", 2},
|
||
3: {"spirit_stone_mid", 10},
|
||
4: {"soul_crystal", 20},
|
||
5: {"immortal_crystal", 55},
|
||
6: {"chaos_crystal", 140},
|
||
}
|
||
key := fmt.Sprintf("combat.drop.currency_tier%d", realmTier)
|
||
if v, ok := cfgGet(key); ok {
|
||
if m, ok2 := v.(map[string]interface{}); ok2 {
|
||
code, _ := m["code"].(string)
|
||
amount := toFloat(m["amount"], 0)
|
||
if code != "" && amount > 0 {
|
||
return code, amount
|
||
}
|
||
}
|
||
}
|
||
if d, ok := defaults[realmTier]; ok {
|
||
return d.code, d.amount
|
||
}
|
||
return "copper", 10
|
||
}
|
||
|
||
// MarshalReport 将战报序列化为 JSONB 字节
|
||
func (r *BattleReport) MarshalReport() ([]byte, error) {
|
||
return json.Marshal(r)
|
||
}
|