lawless/server/internal/battle/entity.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"` // 总 CDticks
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"
}
}