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"
|
|||
|
|
}
|
|||
|
|
}
|