344 行
7.5 KiB
Go
344 行
7.5 KiB
Go
package battle
|
||
|
||
import (
|
||
"encoding/json"
|
||
"math"
|
||
)
|
||
|
||
// DamageType 伤害类型:物理 / 法术 / 真实
|
||
type DamageType string
|
||
|
||
const (
|
||
DamageTypePhysical DamageType = "physical"
|
||
DamageTypeMagical DamageType = "magical"
|
||
DamageTypeTrue DamageType = "true"
|
||
)
|
||
|
||
// TargetType 技能目标类型
|
||
type TargetType string
|
||
|
||
const (
|
||
TargetSingle TargetType = "single"
|
||
TargetSelf TargetType = "self"
|
||
TargetAlly TargetType = "ally"
|
||
)
|
||
|
||
// BaseStats 角色六维基础素质(与 DB base_stats JSONB 对齐)
|
||
type BaseStats struct {
|
||
Str float64 `json:"str"`
|
||
Vit float64 `json:"vit"`
|
||
Wis float64 `json:"wis"`
|
||
Agi float64 `json:"agi"`
|
||
Spi float64 `json:"spi"`
|
||
Luk float64 `json:"luk"`
|
||
// Blood 为巫族特殊属性,可替换灵
|
||
Blood float64 `json:"blood,omitempty"`
|
||
}
|
||
|
||
// SkillInstance 战斗中的一次技能实例(包含运行时 CD)
|
||
type SkillInstance struct {
|
||
InstanceID string `json:"instance_id"`
|
||
SkillID string `json:"skill_id"`
|
||
Name string `json:"name"`
|
||
DamageType DamageType `json:"damage_type"`
|
||
Element string `json:"element"` // fire/water/lightning/earth/nature/yin/yang/chaos/none
|
||
Alignment string `json:"alignment"` // light/dark/neutral
|
||
ScalingAttr string `json:"scaling_attr"` // str/vit/wis/agi/spi/luk/blood
|
||
BaseCoef float64 `json:"base_coef"`
|
||
CD int `json:"cd"` // 总 CD(ticks)
|
||
Cost float64 `json:"cost"` // 内力消耗
|
||
TriggerRate float64 `json:"trigger_rate"` // 基础触发率,1.0=100%
|
||
TargetType TargetType `json:"target_type"`
|
||
|
||
// 运行时字段
|
||
CDRemaining int `json:"-"`
|
||
}
|
||
|
||
// IsNormal 判断是否为普通攻击兜底技能
|
||
func (s *SkillInstance) IsNormal() bool {
|
||
return s == nil || s.SkillID == "normal_attack"
|
||
}
|
||
|
||
// Status 简易战斗状态
|
||
type Status struct {
|
||
Type string
|
||
Stacks int
|
||
Duration int
|
||
}
|
||
|
||
// Fighter 战斗实体(玩家或怪物)
|
||
type Fighter struct {
|
||
ID string
|
||
Name string
|
||
RaceID string
|
||
Job string
|
||
Side string // attacker / defender
|
||
IsPlayer bool
|
||
|
||
// 基础素质
|
||
Base BaseStats
|
||
|
||
// 派生战斗属性
|
||
HPMax float64
|
||
HP float64
|
||
PhysAtk float64
|
||
MagAtk float64
|
||
Speed float64
|
||
CritRate float64
|
||
EvadeRate float64
|
||
PhysDR float64
|
||
MagDR float64
|
||
|
||
// 战斗资源(内力)
|
||
Mana float64
|
||
ManaMax float64
|
||
|
||
// 阵营 / 元素
|
||
WorldTier int
|
||
RealmTier int
|
||
Alignment string
|
||
Element string
|
||
|
||
// 技能
|
||
Skills []*SkillInstance
|
||
|
||
// 状态
|
||
Stunned bool
|
||
StunRemaining int
|
||
SlowRemaining int
|
||
|
||
// ATB
|
||
Gauge float64
|
||
ActionCount int
|
||
}
|
||
|
||
// IsAlive 是否存活
|
||
func (f *Fighter) IsAlive() bool {
|
||
return f != nil && f.HP > 0
|
||
}
|
||
|
||
// EffectiveSpeed 计算当前有效速度(含减速)
|
||
func (f *Fighter) EffectiveSpeed() float64 {
|
||
speed := f.Speed
|
||
if f.SlowRemaining > 0 {
|
||
slow := cfgFloat("combat.cc.slow_value", 0.2)
|
||
speed *= (1 - slow)
|
||
}
|
||
if speed < 1 {
|
||
speed = 1
|
||
}
|
||
return speed
|
||
}
|
||
|
||
// ResetGauge 行动后清空行动条
|
||
func (f *Fighter) ResetGauge() {
|
||
f.Gauge = 0
|
||
}
|
||
|
||
// ReduceCooldowns 每 tick 减少技能 CD
|
||
func (f *Fighter) ReduceCooldowns() {
|
||
for _, s := range f.Skills {
|
||
if s.CDRemaining > 0 {
|
||
s.CDRemaining--
|
||
}
|
||
}
|
||
}
|
||
|
||
// AddCooldown 为指定技能进入 CD
|
||
func (f *Fighter) AddCooldown(s *SkillInstance) {
|
||
if s == nil || s.CD <= 0 {
|
||
return
|
||
}
|
||
for _, sk := range f.Skills {
|
||
if sk.InstanceID == s.InstanceID && sk.SkillID == s.SkillID {
|
||
sk.CDRemaining = s.CD
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// RegenMana 每 tick 回复内力
|
||
func (f *Fighter) RegenMana() {
|
||
regen := cfgFloat("combat.mana.regen_per_tick", 0.15)
|
||
f.Mana += regen
|
||
if f.Mana > f.ManaMax {
|
||
f.Mana = f.ManaMax
|
||
}
|
||
}
|
||
|
||
// ManaRegenAfterAction 行动后额外回复内力
|
||
func (f *Fighter) ManaRegenAfterAction() {
|
||
regen := cfgFloat("combat.mana.regen_per_action", 3)
|
||
f.Mana += regen
|
||
if f.Mana > f.ManaMax {
|
||
f.Mana = f.ManaMax
|
||
}
|
||
}
|
||
|
||
// CanSpendMana 是否有足够内力释放技能
|
||
func (f *Fighter) CanSpendMana(cost float64) bool {
|
||
return f.Mana >= cost
|
||
}
|
||
|
||
// SpendMana 消耗内力
|
||
func (f *Fighter) SpendMana(cost float64) {
|
||
f.Mana -= cost
|
||
if f.Mana < 0 {
|
||
f.Mana = 0
|
||
}
|
||
}
|
||
|
||
// TakeDamage 受到伤害
|
||
func (f *Fighter) TakeDamage(dmg float64) {
|
||
f.HP -= dmg
|
||
if f.HP < 0 {
|
||
f.HP = 0
|
||
}
|
||
}
|
||
|
||
// AddSkill 添加技能
|
||
func (f *Fighter) AddSkill(s *SkillInstance) {
|
||
if s == nil {
|
||
return
|
||
}
|
||
f.Skills = append(f.Skills, s)
|
||
}
|
||
|
||
// ActiveSkillIDs 返回用于战报展示的技能 ID 列表
|
||
func (f *Fighter) ActiveSkillIDs() []string {
|
||
ids := make([]string, 0, len(f.Skills))
|
||
for _, s := range f.Skills {
|
||
if s.SkillID != "normal_attack" {
|
||
ids = append(ids, s.SkillID)
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// NewFighter 通过基础属性创建一个战斗实体
|
||
func NewFighter(id, name, raceID, side string, worldTier, realmTier int, base BaseStats, alignment, element string, isPlayer bool) *Fighter {
|
||
f := &Fighter{
|
||
ID: id,
|
||
Name: name,
|
||
RaceID: raceID,
|
||
Side: side,
|
||
IsPlayer: isPlayer,
|
||
Base: base,
|
||
WorldTier: worldTier,
|
||
RealmTier: realmTier,
|
||
Alignment: alignment,
|
||
Element: element,
|
||
}
|
||
f.recalcDerived()
|
||
f.ManaMax = cfgFloat("combat.mana.max", 100)
|
||
f.Mana = f.ManaMax
|
||
return f
|
||
}
|
||
|
||
// NewFighterFromJSON 从 DB JSONB 解析基础属性后创建实体
|
||
func NewFighterFromJSON(id, name, raceID, side string, worldTier, realmTier int, data []byte, alignment, element string, isPlayer bool) *Fighter {
|
||
var base BaseStats
|
||
_ = json.Unmarshal(data, &base)
|
||
return NewFighter(id, name, raceID, side, worldTier, realmTier, base, alignment, element, isPlayer)
|
||
}
|
||
|
||
// recalcDerived 根据基础素质重算战斗属性
|
||
func (f *Fighter) recalcDerived() {
|
||
hpCoef := hpCoefForTier(f.WorldTier)
|
||
f.HPMax = f.Base.Vit*hpCoef + realmBaseHP(f.RealmTier)
|
||
f.HP = f.HPMax
|
||
|
||
atkCoef := cfgFloat("combat.atk.coefficient", 1.0)
|
||
magAtkCoef := cfgFloat("combat.magic_atk.coefficient", 1.0)
|
||
f.PhysAtk = f.Base.Str*atkCoef
|
||
f.MagAtk = f.Base.Spi*magAtkCoef
|
||
if f.Base.Blood > 0 {
|
||
// 巫族可用血属性部分替代灵
|
||
f.MagAtk += f.Base.Blood * magAtkCoef * 0.5
|
||
f.PhysAtk += f.Base.Blood * atkCoef * 0.5
|
||
}
|
||
|
||
f.Speed = f.Base.Agi
|
||
|
||
critCap := cfgFloat("combat.crit.rate_cap", 0.6)
|
||
f.CritRate = math.Min(critCap, f.Base.Luk*0.0015)
|
||
|
||
evaCap := cfgFloat("combat.evade.rate_cap", 0.5)
|
||
f.EvadeRate = math.Min(evaCap, f.Base.Agi*0.001)
|
||
|
||
defCoef := cfgFloat("combat.defense.coefficient", 0.5)
|
||
defConst := defenseConstantForTier(f.WorldTier)
|
||
f.PhysDR = math.Min(0.75, f.Base.Vit*defCoef/(f.Base.Vit*defCoef+defConst))
|
||
f.MagDR = math.Min(0.75, f.Base.Spi*defCoef/(f.Base.Spi*defCoef+defConst))
|
||
}
|
||
|
||
func realmBaseHP(realmTier int) float64 {
|
||
switch realmTier {
|
||
case 1:
|
||
return 100
|
||
case 2:
|
||
return 300
|
||
case 3:
|
||
return 700
|
||
case 4:
|
||
return 1500
|
||
case 5:
|
||
return 3000
|
||
case 6:
|
||
return 6000
|
||
default:
|
||
return 100
|
||
}
|
||
}
|
||
|
||
func hpCoefForTier(worldTier int) float64 {
|
||
defaults := map[int]float64{
|
||
1: 10,
|
||
2: 12,
|
||
3: 15,
|
||
4: 20,
|
||
5: 28,
|
||
6: 38,
|
||
}
|
||
key := "combat.hp.coefficient_tier_" + tierKey(worldTier)
|
||
if v, ok := cfgGet(key); ok {
|
||
return toFloat(v, defaults[worldTier])
|
||
}
|
||
return defaults[worldTier]
|
||
}
|
||
|
||
func defenseConstantForTier(worldTier int) float64 {
|
||
defaults := map[int]float64{
|
||
1: 100,
|
||
2: 150,
|
||
3: 220,
|
||
4: 320,
|
||
5: 460,
|
||
6: 640,
|
||
}
|
||
key := "combat.defense.constant_tier_" + tierKey(worldTier)
|
||
if v, ok := cfgGet(key); ok {
|
||
return toFloat(v, defaults[worldTier])
|
||
}
|
||
return defaults[worldTier]
|
||
}
|
||
|
||
func tierKey(tier int) string {
|
||
switch tier {
|
||
case 1:
|
||
return "1"
|
||
case 2:
|
||
return "2"
|
||
case 3:
|
||
return "3"
|
||
case 4:
|
||
return "4"
|
||
case 5:
|
||
return "5"
|
||
case 6:
|
||
return "6"
|
||
default:
|
||
return "1"
|
||
}
|
||
}
|