武器、子弹、普通伤害、aoe伤害、减速等
package game
import (
"fmt"
"log"
"math"
"math/rand"
"sync"
"time"
)
// Position 坐标位置
type Position struct {
X float64
Y float64
}
// Distance 计算两点之间的距离
func (p1 Position) Distance(p2 Position) float64 {
return math.Sqrt(math.Pow(p1.X-p2.X, 2) + math.Pow(p1.Y-p2.Y, 2))
}
// MonsterType 怪物类型
type MonsterType string
const (
MonsterTypeSlime MonsterType = "slime"
MonsterTypeGoblin MonsterType = "goblin"
MonsterTypeOrc MonsterType = "orc"
MonsterTypeDragon MonsterType = "dragon"
)
// Monster 怪物结构体
type Monster struct {
ID string
Type MonsterType
HP int
MaxHP int
Position Position
Speed float64 // 基础速度
CurrentSpeed float64 // 当前速度(受减速效果影响)
SpeedReduce float64 // 减速比例(0.0-1.0)
SpeedReduceEnd time.Time // 减速结束时间
Stunned bool // 眩晕状态
StunnedEnd time.Time // 眩晕结束时间
Burning bool // 燃烧状态
BurnEnd time.Time // 燃烧结束时间
BurnDamage int // 每秒燃烧伤害
}
// Player 玩家结构体
type Player struct {
ID string
HP int
MaxHP int
Position Position
Attack int
Weapon *Weapon
}
// WeaponType 武器类型
type WeaponType string
const (
WeaponTypePistol WeaponType = "pistol" // 手枪 - 直线单发射击
WeaponTypeShotgun WeaponType = "shotgun" // xian弹枪 - 扇形射击
WeaponTypeRifle WeaponType = "rifle" // 步枪 - 直线穿透
WeaponTypeSniper WeaponType = "sniper" // 狙击枪 - 高伤害直线
WeaponTypeMachineGun WeaponType = "machinegun" // 机枪 - 快速连射
)
// BulletType 子弹类型
type BulletType string
const (
BulletTypeNormal BulletType = "normal" // 普通子弹 - 直线
BulletTypeShotgun BulletType = "shotgun" // xian弹 - 扇形扩散
BulletTypePiercing BulletType = "piercing" // 穿透子弹
BulletTypeSniper BulletType = "sniper" // 狙击子弹
)
// EffectType 范围效果类型
type EffectType string
const (
EffectTypeFire EffectType = "fire" // 火焰 - 范围持续伤害
EffectTypePoison EffectType = "poison" // 毒 - 范围持续伤害
EffectTypeIce EffectType = "ice" // 冰冻 - 减速效果
EffectTypeStun EffectType = "stun" // 眩晕 - 定身效果
EffectTypeBurn EffectType = "burn" // 燃烧 - 挂载型持续伤害(挂载在怪物身上)
)
// AreaEffect 范围效果结构体
type AreaEffect struct {
ID string // 效果ID
Type EffectType // 效果类型
Position Position // 中心位置
Radius float64 // 范围半径
Damage int // 每跳伤害
TickInterval time.Duration // 伤害间隔
Duration time.Duration // 持续时间
Elapsed time.Duration // 已过时间
LastTick time.Duration // 上次伤害时间
Active bool // 是否激活
SpeedReduce float64 // 减速比例(冰冻效果用,0.0-1.0)
}
// EffectStrategy 范围效果策略接口
type EffectStrategy interface {
OnTick(effect *AreaEffect, monster *Monster, game *Game) bool // 返回是否造成伤害
OnCreate(effect *AreaEffect)
GetName() string
}
// FireStrategy 火焰效果策略
type FireStrategy struct{}
func (s *FireStrategy) OnTick(effect *AreaEffect, monster *Monster, game *Game) bool {
monster.HP -= effect.Damage
log.Printf("火焰效果对怪物造成 %d 点伤害,剩余血量: %d", effect.Damage, monster.HP)
return monster.HP <= 0
}
func (s *FireStrategy) OnCreate(effect *AreaEffect) {
log.Printf("火焰效果已创建,范围: %.0f,持续时间: %v", effect.Radius, effect.Duration)
}
func (s *FireStrategy) GetName() string { return "火焰" }
// PoisonStrategy 毒效果策略
type PoisonStrategy struct{}
func (s *PoisonStrategy) OnTick(effect *AreaEffect, monster *Monster, game *Game) bool {
monster.HP -= effect.Damage
log.Printf("毒效果对怪物造成 %d 点伤害,剩余血量: %d", effect.Damage, monster.HP)
return monster.HP <= 0
}
func (s *PoisonStrategy) OnCreate(effect *AreaEffect) {
log.Printf("毒效果已创建,范围: %.0f,持续时间: %v", effect.Radius, effect.Duration)
}
func (s *PoisonStrategy) GetName() string { return "毒" }
// IceStrategy 冰冻效果策略
type IceStrategy struct{}
func (s *IceStrategy) OnTick(effect *AreaEffect, monster *Monster, game *Game) bool {
// 冰冻效果不造成伤害,只减速
// 减速逻辑已在updateAreaEffects中处理
return false // 不造成死亡
}
func (s *IceStrategy) OnCreate(effect *AreaEffect) {
log.Printf("冰冻效果已创建,范围: %.0f,持续时间: %v,减速: %.0f%%",
effect.Radius, effect.Duration, effect.SpeedReduce*100)
}
func (s *IceStrategy) GetName() string { return "冰冻" }
// StunStrategy 眩晕效果策略
type StunStrategy struct{}
func (s *StunStrategy) OnTick(effect *AreaEffect, monster *Monster, game *Game) bool {
// 眩晕效果不造成伤害,只眩晕
// 眩晕逻辑已在updateAreaEffects中处理
return false // 不造成死亡
}
func (s *StunStrategy) OnCreate(effect *AreaEffect) {
log.Printf("眩晕效果已创建,范围: %.0f,持续时间: %v", effect.Radius, effect.Duration)
}
func (s *StunStrategy) GetName() string { return "眩晕" }
// BurnStrategy 燃烧效果策略(挂载型)
type BurnStrategy struct{}
func (s *BurnStrategy) OnTick(effect *AreaEffect, monster *Monster, game *Game) bool {
// 燃烧效果不通过范围效果Tick处理,而是挂载在怪物身上
// 实际燃烧伤害在updateMonsters中处理
return false
}
func (s *BurnStrategy) OnCreate(effect *AreaEffect) {
log.Printf("燃烧效果已创建,范围: %.0f,持续时间: %v", effect.Radius, effect.Duration)
}
func (s *BurnStrategy) GetName() string { return "燃烧" }
// GetEffectStrategy 获取效果策略
func GetEffectStrategy(effectType EffectType) EffectStrategy {
switch effectType {
case EffectTypeFire:
return &FireStrategy{}
case EffectTypePoison:
return &PoisonStrategy{}
case EffectTypeIce:
return &IceStrategy{}
case EffectTypeStun:
return &StunStrategy{}
case EffectTypeBurn:
return &BurnStrategy{}
default:
return &FireStrategy{}
}
}
// Weapon 武器结构体
type Weapon struct {
ID string
Name string
Type WeaponType
Damage int // 基础伤害
Range float64 // 射程
BulletSpeed float64 // 子弹速度
Penetration bool // 是否穿透
MaxPenetration int // 最大穿透数量
BulletCount int // 每次发射子弹数量(xian弹枪用)
SpreadAngle float64 // 扩散角度(弧度,xian弹枪用)
FireRate time.Duration // 射速间隔
Level int // 武器等级
DamageMultiplier float64 // 伤害倍率(随等级提升)
BulletType BulletType // 子弹类型
Knockback float64 // 击退力度
// 暴击系统
CriticalChance float64 // 暴击率(0.0-1.0)
CriticalMultiplier float64 // 暴击伤害倍率
// 弹药系统
MaxAmmo int // 最大弹药量
CurrentAmmo int // 当前弹药量
ReloadTime time.Duration // 换弹时间
}
// ShootingStrategy 射击策略接口
type ShootingStrategy interface {
Shoot(g *Game, weapon *Weapon, targetX, targetY float64)
}
// NormalStrategy 普通直线射击策略
type NormalStrategy struct{}
func (s *NormalStrategy) Shoot(g *Game, weapon *Weapon, targetX, targetY float64) {
g.shootInternal(targetX, targetY, weapon)
}
// ShotgunStrategy xian弹枪扇形射击策略
type ShotgunStrategy struct{}
func (s *ShotgunStrategy) Shoot(g *Game, weapon *Weapon, targetX, targetY float64) {
playerPos := g.player.Position
// 计算基础方向
dx := targetX - playerPos.X
dy := targetY - playerPos.Y
distance := math.Sqrt(dx*dx + dy*dy)
if distance < 0.001 {
return
}
baseDirX := dx / distance
baseDirY := dy / distance
// 使用公共方法计算扇形角度
angles := CalculateSpreadAngles(weapon.BulletCount, weapon.SpreadAngle)
// 生成扇形分布的子弹
for _, angleOffset := range angles {
// 使用公共方法计算方向
dirX, dirY := CalculateDirectionWithSpread(baseDirX, baseDirY, angleOffset)
// 使用公共方法创建子弹
g.CreateBullet(playerPos.X, playerPos.Y, dirX, dirY, weapon)
}
}
// GetShootingStrategy 获取射击策略
func GetShootingStrategy(weaponType WeaponType) ShootingStrategy {
switch weaponType {
case WeaponTypeShotgun:
return &ShotgunStrategy{}
case WeaponTypePistol, WeaponTypeRifle, WeaponTypeSniper, WeaponTypeMachineGun:
default:
return &NormalStrategy{}
}
return &NormalStrategy{}
}
// CalculateDamage 计算武器最终伤害
func CalculateDamage(weapon *Weapon) int {
return int(float64(weapon.Damage) * weapon.DamageMultiplier)
}
// CalculateDamageMultiplier 计算伤害倍率(根据等级)
func CalculateDamageMultiplier(level int) float64 {
return 1.0 + float64(level-1)*0.2 // 每级提升20%
}
// CalculateSpreadAngles 计算扇形分布的角度列表
// bulletCount: 子弹数量
// spreadAngle: 总扩散角度(弧度)
func CalculateSpreadAngles(bulletCount int, spreadAngle float64) []float64 {
if bulletCount <= 1 {
return []float64{0}
}
angles := make([]float64, bulletCount)
halfAngle := spreadAngle / 2
for i := 0; i < bulletCount; i++ {
if bulletCount == 1 {
angles[i] = 0
} else {
angles[i] = halfAngle*2*(float64(i)/(float64(bulletCount)-1)) - halfAngle
}
}
return angles
}
// CalculateDirectionWithSpread 计算带角度偏移的方向向量
func CalculateDirectionWithSpread(baseDirX, baseDirY, angleOffset float64) (float64, float64) {
cos := math.Cos(angleOffset)
sin := math.Sin(angleOffset)
return baseDirX*cos - baseDirY*sin, baseDirX*sin + baseDirY*cos
}
// CreateBullet 创建子弹
func (g *Game) CreateBullet(x, y, dirX, dirY float64, weapon *Weapon) {
damage := CalculateDamage(weapon)
isCritical := false
// 暴击判定
if weapon.CriticalChance > 0 && rand.Float64() < weapon.CriticalChance {
damage = int(float64(damage) * weapon.CriticalMultiplier)
isCritical = true
}
bullet := &Bullet{
ID: "bullet_" + string(rune('a'+g.bulletID)),
Position: Position{X: x, Y: y},
Direction: Position{X: dirX, Y: dirY},
Speed: weapon.BulletSpeed,
Damage: damage,
Penetration: weapon.Penetration,
MaxPenetration: weapon.MaxPenetration,
RemainingPenetrations: weapon.MaxPenetration,
Active: true,
BulletType: weapon.BulletType,
Knockback: weapon.Knockback,
Critical: isCritical, // 标记是否为暴击
}
g.bulletID++
g.bullets[bullet.ID] = bullet
if isCritical {
log.Printf("创建暴击子弹ID: %s,方向: (%f, %f),暴击伤害: %d", bullet.ID, dirX, dirY, damage)
} else {
log.Printf("创建子弹ID: %s,方向: (%f, %f),伤害: %d", bullet.ID, dirX, dirY, damage)
}
}
// Bullet 子弹结构体
type Bullet struct {
ID string
Position Position
Direction Position // 移动方向(单位向量)
Speed float64
Damage int
Penetration bool
MaxPenetration int
RemainingPenetrations int // 剩余穿透次数
Active bool
BulletType BulletType
Knockback float64 // 击退力度
Critical bool // 是否为暴击
}
// GameConfig 游戏配置
type GameConfig struct {
MapWidth float64
MapHeight float64
MonsterSpawnInterval time.Duration
UpdateInterval time.Duration // 游戏状态更新间隔
AttackRange float64
PlayerStartHP int
AutoShootRange float64 // 自动射击范围
AutoShootInterval time.Duration // 自动射击间隔
}
// Game 游戏核心结构体
type Game struct {
config GameConfig
player *Player
monsters map[string]*Monster
bullets map[string]*Bullet
areaEffects map[string]*AreaEffect // 范围效果列表
mu sync.RWMutex
running bool
spawnTicker *time.Ticker // 怪物生成定时器
updateTicker *time.Ticker // 游戏状态更新定时器
autoShootTicker *time.Ticker // 自动射击定时器
gameOver bool
monsterID int
bulletID int
effectID int
lastFireTime map[string]time.Time // 每种武器的上次射击时间(用于冷却控制)
reloading map[string]bool // 每种武器是否正在换弹
}
// NewGame 创建新游戏
func NewGame(config GameConfig) *Game {
// 设置默认值(-1表示关闭自动射击)
if config.AutoShootRange < 0 {
// 负值表示关闭自动射击,不设置默认值
} else if config.AutoShootRange == 0 {
config.AutoShootRange = 200 // 默认自动射击范围
}
if config.AutoShootInterval <= 0 {
config.AutoShootInterval = 500 * time.Millisecond // 默认自动射击间隔
}
// AutoShootRange < 0 表示关闭自动射击
return &Game{
config: config,
monsters: make(map[string]*Monster),
bullets: make(map[string]*Bullet),
areaEffects: make(map[string]*AreaEffect),
player: &Player{
ID: "player",
HP: config.PlayerStartHP,
MaxHP: config.PlayerStartHP,
Position: Position{X: config.MapWidth / 2, Y: config.MapHeight / 2},
Attack: 10,
Weapon: NewWeapon(WeaponTypePistol, 1), // 初始武器:1级手枪
},
lastFireTime: make(map[string]time.Time),
reloading: make(map[string]bool),
}
}
// NewWeapon 创建武器
func NewWeapon(weaponType WeaponType, level int) *Weapon {
weapon := &Weapon{
Type: weaponType,
Level: level,
DamageMultiplier: CalculateDamageMultiplier(level), // 使用公共方法计算伤害倍率
}
switch weaponType {
case WeaponTypePistol:
weapon.ID = "weapon_pistol"
weapon.Name = "手枪"
weapon.Damage = 15
weapon.Range = 300
weapon.BulletSpeed = 15
weapon.Penetration = false
weapon.MaxPenetration = 1
weapon.BulletCount = 1
weapon.SpreadAngle = 0
weapon.FireRate = 500 * time.Millisecond
weapon.BulletType = BulletTypeNormal
weapon.CriticalChance = 0.1 // 10%暴击率
weapon.CriticalMultiplier = 2.0 // 暴击伤害2倍
weapon.MaxAmmo = 12 // 12发弹药
weapon.ReloadTime = 1500 * time.Millisecond
case WeaponTypeShotgun:
weapon.ID = "weapon_shotgun"
weapon.Name = "xian弹枪"
weapon.Damage = 10
weapon.Range = 150
weapon.BulletSpeed = 12
weapon.Penetration = false
weapon.MaxPenetration = 1
weapon.BulletCount = 5
weapon.SpreadAngle = math.Pi / 6 // 30度扇形
weapon.FireRate = 1000 * time.Millisecond
weapon.BulletType = BulletTypeShotgun
weapon.Knockback = 30 // xian弹枪有较大击退力
weapon.CriticalChance = 0.05 // 5%暴击率
weapon.CriticalMultiplier = 1.5 // 暴击伤害1.5倍
weapon.MaxAmmo = 6 // 6发弹药
weapon.ReloadTime = 2000 * time.Millisecond
case WeaponTypeRifle:
weapon.ID = "weapon_rifle"
weapon.Name = "步枪"
weapon.Damage = 25
weapon.Range = 400
weapon.BulletSpeed = 25
weapon.Penetration = true
weapon.MaxPenetration = 3
weapon.BulletCount = 1
weapon.SpreadAngle = 0
weapon.FireRate = 600 * time.Millisecond
weapon.BulletType = BulletTypePiercing
weapon.Knockback = 10 // 步枪有小击退力
weapon.CriticalChance = 0.15 // 15%暴击率
weapon.CriticalMultiplier = 2.0 // 暴击伤害2倍
weapon.MaxAmmo = 30 // 30发弹药
weapon.ReloadTime = 1500 * time.Millisecond
case WeaponTypeSniper:
weapon.ID = "weapon_sniper"
weapon.Name = "狙击枪"
weapon.Damage = 50
weapon.Range = 600
weapon.BulletSpeed = 40
weapon.Penetration = true
weapon.MaxPenetration = 2
weapon.BulletCount = 1
weapon.SpreadAngle = 0
weapon.FireRate = 2000 * time.Millisecond
weapon.BulletType = BulletTypeSniper
weapon.Knockback = 50 // 狙击枪有极大击退力
weapon.CriticalChance = 0.3 // 30%暴击率
weapon.CriticalMultiplier = 2.5 // 暴击伤害2.5倍
weapon.MaxAmmo = 5 // 5发弹药
weapon.ReloadTime = 3000 * time.Millisecond
case WeaponTypeMachineGun:
weapon.ID = "weapon_machinegun"
weapon.Name = "机枪"
weapon.Damage = 8
weapon.Range = 250
weapon.BulletSpeed = 20
weapon.Penetration = false
weapon.MaxPenetration = 1
weapon.BulletCount = 1
weapon.SpreadAngle = 0
weapon.FireRate = 100 * time.Millisecond
weapon.BulletType = BulletTypeNormal
weapon.CriticalChance = 0.05 // 5%暴击率
weapon.CriticalMultiplier = 1.5 // 暴击伤害1.5倍
weapon.MaxAmmo = 100 // 100发弹药
weapon.ReloadTime = 2500 * time.Millisecond
}
// 初始化当前弹药为最大弹药
weapon.CurrentAmmo = weapon.MaxAmmo
return weapon
}
// GetPlayer 获取玩家
func (g *Game) GetPlayer() *Player {
g.mu.RLock()
defer g.mu.RUnlock()
return g.player
}
// GetMonsters 获取所有怪物
func (g *Game) GetMonsters() []*Monster {
g.mu.RLock()
defer g.mu.RUnlock()
result := make([]*Monster, 0, len(g.monsters))
for _, m := range g.monsters {
result = append(result, m)
}
return result
}
// MovePlayer 移动玩家
// dx: X方向移动量(正数向右,负数向左)
// dy: Y方向移动量(正数向下,负数向上)
func (g *Game) MovePlayer(dx, dy float64) {
g.mu.Lock()
defer g.mu.Unlock()
// 计算新位置
newX := g.player.Position.X + dx
newY := g.player.Position.Y + dy
// 确保玩家不会移出地图边界
if newX < 0 {
newX = 0
}
if newX > g.config.MapWidth {
newX = g.config.MapWidth
}
if newY < 0 {
newY = 0
}
if newY > g.config.MapHeight {
newY = g.config.MapHeight
}
// 更新玩家位置
g.player.Position.X = newX
g.player.Position.Y = newY
}
// Shoot 玩家射击(公共方法)
// targetX, targetY: 目标坐标
func (g *Game) Shoot(targetX, targetY float64) {
g.mu.Lock()
defer g.mu.Unlock()
if g.gameOver || g.player.Weapon == nil {
return
}
weapon := g.player.Weapon
now := time.Now()
// 检查是否正在换弹
if g.reloading[string(weapon.Type)] {
log.Printf("%s 正在换弹中,无法射击", weapon.Name)
return
}
// 检查弹药
if weapon.CurrentAmmo <= 0 {
log.Printf("%s 弹药耗尽,自动换弹中", weapon.Name)
go g.reload(weapon)
return
}
// 检查冷却
if lastTime, exists := g.lastFireTime[string(weapon.Type)]; exists {
if now.Sub(lastTime) < weapon.FireRate {
return // 冷却中
}
}
// 更新射击时间
g.lastFireTime[string(weapon.Type)] = now
// 消耗弹药
weapon.CurrentAmmo--
// 获取对应的射击策略并执行
strategy := GetShootingStrategy(weapon.Type)
strategy.Shoot(g, weapon, targetX, targetY)
}
// reload 换弹方法
func (g *Game) reload(weapon *Weapon) {
g.mu.Lock()
g.reloading[string(weapon.Type)] = true
g.mu.Unlock()
// 等待换弹时间
time.Sleep(weapon.ReloadTime)
g.mu.Lock()
defer g.mu.Unlock()
// 换弹完成
weapon.CurrentAmmo = weapon.MaxAmmo
g.reloading[string(weapon.Type)] = false
log.Printf("%s 换弹完成,当前弹药: %d/%d", weapon.Name, weapon.CurrentAmmo, weapon.MaxAmmo)
}
// shootInternal 内部射击方法(已持有锁)- 用于普通直线射击
func (g *Game) shootInternal(targetX, targetY float64, weapon *Weapon) {
playerPos := g.player.Position
// 计算射击方向
dx := targetX - playerPos.X
dy := targetY - playerPos.Y
distance := math.Sqrt(dx*dx + dy*dy)
if distance < 0.001 {
return // 避免除以零
}
// 标准化方向向量
dirX := dx / distance
dirY := dy / distance
// 使用公共方法创建子弹
g.CreateBullet(playerPos.X, playerPos.Y, dirX, dirY, weapon)
}
// ToggleWeaponPenetration 切换武器穿透功能
func (g *Game) ToggleWeaponPenetration() {
g.mu.Lock()
defer g.mu.Unlock()
if g.player.Weapon != nil {
g.player.Weapon.Penetration = !g.player.Weapon.Penetration
log.Printf("武器穿透功能 %s", map[bool]string{true: "开启", false: "关闭"}[g.player.Weapon.Penetration])
}
}
// Start 开始游戏
func (g *Game) Start() {
g.mu.Lock()
if g.running {
g.mu.Unlock()
return
}
g.running = true
g.gameOver = false
g.mu.Unlock()
// 启动怪物生成定时器
g.spawnTicker = time.NewTicker(g.config.MonsterSpawnInterval)
go func() {
for {
g.mu.Lock()
if !g.running {
g.mu.Unlock()
return
}
g.mu.Unlock()
<-g.spawnTicker.C
g.spawnMonster()
}
}()
// 启动游戏状态更新定时器
g.updateTicker = time.NewTicker(g.config.UpdateInterval)
go func() {
for {
g.mu.Lock()
if !g.running {
g.mu.Unlock()
return
}
g.mu.Unlock()
<-g.updateTicker.C
g.updateInternal()
}
}()
// 启动自动射击定时器
g.autoShootTicker = time.NewTicker(g.config.AutoShootInterval)
go func() {
for {
g.mu.Lock()
if !g.running {
g.mu.Unlock()
return
}
g.mu.Unlock()
<-g.autoShootTicker.C
g.autoShoot()
}
}()
}
// autoShoot 自动射击:向范围内最近的怪物射击
func (g *Game) autoShoot() {
g.mu.Lock()
defer g.mu.Unlock()
if g.gameOver || g.player.Weapon == nil {
return
}
// 如果AutoShootRange < 0,表示关闭自动射击
if g.config.AutoShootRange < 0 {
return
}
playerPos := g.player.Position
shootRange := g.config.AutoShootRange
// 寻找范围内最近的怪物
var nearestMonster *Monster
minDistance := shootRange + 1 // 初始值设为超过范围
for _, monster := range g.monsters {
distance := playerPos.Distance(monster.Position)
if distance <= shootRange && distance < minDistance {
minDistance = distance
nearestMonster = monster
}
}
// 如果找到目标,射击(使用策略模式)
if nearestMonster != nil {
strategy := GetShootingStrategy(g.player.Weapon.Type)
strategy.Shoot(g, g.player.Weapon, nearestMonster.Position.X, nearestMonster.Position.Y)
}
}
// CreateAreaEffect 创建范围效果
// effectType: 效果类型(火焰、毒等)
// x, y: 效果中心坐标
// radius: 范围半径
// damage: 每跳伤害
// tickInterval: 伤害间隔
// duration: 持续时间
func (g *Game) CreateAreaEffect(effectType EffectType, x, y float64, radius float64, damage int, tickInterval, duration time.Duration) {
g.mu.Lock()
defer g.mu.Unlock()
effect := &AreaEffect{
ID: fmt.Sprintf("effect_%d", g.effectID),
Type: effectType,
Position: Position{X: x, Y: y},
Radius: radius,
Damage: damage,
TickInterval: tickInterval,
Duration: duration,
Active: true,
}
g.effectID++
// 调用效果策略的创建回调
strategy := GetEffectStrategy(effectType)
strategy.OnCreate(effect)
g.areaEffects[effect.ID] = effect
}
// CreateFireEffect 便捷方法:创建火海
func (g *Game) CreateFireEffect(x, y, radius float64, damage int, duration time.Duration) {
g.CreateAreaEffect(EffectTypeFire, x, y, radius, damage, 500*time.Millisecond, duration)
}
// CreatePoisonEffect 便捷方法:创建毒池
func (g *Game) CreatePoisonEffect(x, y, radius float64, damage int, duration time.Duration) {
g.CreateAreaEffect(EffectTypePoison, x, y, radius, damage, 300*time.Millisecond, duration)
}
// CreateIceEffect 便捷方法:创建冰冻效果
// speedReduce: 减速比例(0.0-1.0,0.5表示减速50%)
func (g *Game) CreateIceEffect(x, y, radius float64, speedReduce float64, duration time.Duration) {
g.mu.Lock()
defer g.mu.Unlock()
effect := &AreaEffect{
ID: fmt.Sprintf("effect_%d", g.effectID),
Type: EffectTypeIce,
Position: Position{X: x, Y: y},
Radius: radius,
Damage: 0, // 冰冻不造成伤害
TickInterval: 100 * time.Millisecond, // 减速检测间隔
Duration: duration,
SpeedReduce: speedReduce,
Active: true,
}
g.effectID++
// 调用效果策略的创建回调
strategy := GetEffectStrategy(effect.Type)
strategy.OnCreate(effect)
g.areaEffects[effect.ID] = effect
}
// CreateStunEffect 便捷方法:创建眩晕效果(范围眩晕)
func (g *Game) CreateStunEffect(x, y, radius float64, duration time.Duration) {
g.mu.Lock()
defer g.mu.Unlock()
effect := &AreaEffect{
ID: fmt.Sprintf("effect_%d", g.effectID),
Type: EffectTypeStun,
Position: Position{X: x, Y: y},
Radius: radius,
Damage: 0, // 眩晕不造成伤害
TickInterval: 100 * time.Millisecond, // 检测间隔
Duration: duration,
Active: true,
}
g.effectID++
// 调用效果策略的创建回调
strategy := GetEffectStrategy(effect.Type)
strategy.OnCreate(effect)
g.areaEffects[effect.ID] = effect
}
// CreateBurnEffect 便捷方法:创建燃烧效果(挂载型持续伤害)
// 燃烧效果会挂载在范围内的怪物身上,不依赖范围效果Tick
func (g *Game) CreateBurnEffect(x, y, radius float64, burnDamage int, duration time.Duration) {
g.mu.Lock()
defer g.mu.Unlock()
effect := &AreaEffect{
ID: fmt.Sprintf("effect_%d", g.effectID),
Type: EffectTypeBurn,
Position: Position{X: x, Y: y},
Radius: radius,
Damage: burnDamage,
TickInterval: 500 * time.Millisecond, // 挂载检测间隔
Duration: duration,
Active: true,
}
g.effectID++
// 遍历范围内的怪物,挂载燃烧效果
for _, monster := range g.monsters {
distance := effect.Position.Distance(monster.Position)
if distance <= effect.Radius && !monster.Burning {
monster.Burning = true
monster.BurnEnd = time.Now().Add(effect.Duration)
monster.BurnDamage = effect.Damage
log.Printf("怪物 %s 被挂载燃烧效果,每秒伤害: %d,持续时间: %v", monster.ID, effect.Damage, effect.Duration)
}
}
// 调用效果策略的创建回调
strategy := GetEffectStrategy(effect.Type)
strategy.OnCreate(effect)
g.areaEffects[effect.ID] = effect
}
// Stop 停止游戏
func (g *Game) Stop() {
g.mu.Lock()
defer g.mu.Unlock()
g.running = false
if g.spawnTicker != nil {
g.spawnTicker.Stop()
}
if g.updateTicker != nil {
g.updateTicker.Stop()
}
if g.autoShootTicker != nil {
g.autoShootTicker.Stop()
}
}
// spawnMonster 生成怪物
func (g *Game) spawnMonster() {
g.mu.Lock()
defer g.mu.Unlock()
if g.gameOver {
return
}
// 随机选择怪物类型
monsterTypes := []MonsterType{MonsterTypeSlime, MonsterTypeGoblin, MonsterTypeOrc, MonsterTypeDragon}
monsterType := monsterTypes[rand.Intn(len(monsterTypes))]
// 根据类型设置属性
hp := getMonsterHP(monsterType)
speed := getMonsterSpeed(monsterType)
// 随机生成在地图边缘
position := g.getRandomEdgePosition()
monster := &Monster{
ID: "monster_" + string(rune('a'+g.monsterID)),
Type: monsterType,
HP: hp,
MaxHP: hp,
Position: position,
Speed: speed,
CurrentSpeed: speed, // 初始化当前速度
}
g.monsterID++
g.monsters[monster.ID] = monster
}
// getMonsterHP 获取怪物血量
func getMonsterHP(t MonsterType) int {
switch t {
case MonsterTypeSlime:
return 20
case MonsterTypeGoblin:
return 40
case MonsterTypeOrc:
return 80
case MonsterTypeDragon:
return 200
default:
return 30
}
}
// getMonsterSpeed 获取怪物速度
func getMonsterSpeed(t MonsterType) float64 {
switch t {
case MonsterTypeSlime:
return 1.5
case MonsterTypeGoblin:
return 2.0
case MonsterTypeOrc:
return 1.2
case MonsterTypeDragon:
return 0.8
default:
return 1.5
}
}
// getRandomEdgePosition 获取随机边缘位置
func (g *Game) getRandomEdgePosition() Position {
edge := rand.Intn(4)
switch edge {
case 0: // 上边
return Position{X: rand.Float64() * g.config.MapWidth, Y: 0}
case 1: // 右边
return Position{X: g.config.MapWidth, Y: rand.Float64() * g.config.MapHeight}
case 2: // 下边
return Position{X: rand.Float64() * g.config.MapWidth, Y: g.config.MapHeight}
case 3: // 左边
return Position{X: 0, Y: rand.Float64() * g.config.MapHeight}
default:
return Position{X: 0, Y: 0}
}
}
// Update 更新游戏状态(供外部手动调用)
func (g *Game) Update() {
g.updateInternal()
}
// updateInternal 内部更新游戏状态(由定时器自动调用)
func (g *Game) updateInternal() {
g.mu.Lock()
defer g.mu.Unlock()
if g.gameOver {
return
}
// 移动所有怪物向玩家
for _, monster := range g.monsters {
g.moveMonster(monster)
}
// 更新子弹
g.updateBullets()
// 更新范围效果
g.updateAreaEffects()
// 检测碰撞和攻击
g.checkCombat()
}
// updateBullets 更新所有子弹
func (g *Game) updateBullets() {
// 记录需要删除的子弹
toRemove := make([]string, 0)
for id, bullet := range g.bullets {
if !bullet.Active {
toRemove = append(toRemove, id)
continue
}
// 移动子弹
bullet.Position.X += bullet.Direction.X * bullet.Speed
bullet.Position.Y += bullet.Direction.Y * bullet.Speed
// 检测是否超出地图边界
if bullet.Position.X < 0 || bullet.Position.X > g.config.MapWidth ||
bullet.Position.Y < 0 || bullet.Position.Y > g.config.MapHeight {
toRemove = append(toRemove, id)
continue
}
// 检测子弹与怪物碰撞
for monsterID, monster := range g.monsters {
if distance := bullet.Position.Distance(monster.Position); distance <= 40 { // 子弹碰撞半径增加至40,防止高速子弹跳过碰撞
monster.HP -= bullet.Damage
if bullet.Critical {
log.Printf("暴击!子弹 %s 击中怪物 %s,造成 %d 点暴击伤害,剩余血量: %d", bullet.ID, monsterID, bullet.Damage, monster.HP)
} else {
log.Printf("子弹 %s 击中怪物 %s,造成 %d 点伤害,剩余血量: %d", bullet.ID, monsterID, bullet.Damage, monster.HP)
}
// 击退效果:怪物被子弹击退
if bullet.Knockback > 0 {
// 计算击退后的位置
knockbackX := monster.Position.X + bullet.Direction.X*bullet.Knockback
knockbackY := monster.Position.Y + bullet.Direction.Y*bullet.Knockback
// 限制在地图边界内
if knockbackX < 0 {
knockbackX = 0
}
if knockbackX > g.config.MapWidth {
knockbackX = g.config.MapWidth
}
if knockbackY < 0 {
knockbackY = 0
}
if knockbackY > g.config.MapHeight {
knockbackY = g.config.MapHeight
}
monster.Position.X = knockbackX
monster.Position.Y = knockbackY
log.Printf("怪物 %s 被击退至 (%.2f, %.2f)", monsterID, knockbackX, knockbackY)
}
if monster.HP <= 0 {
delete(g.monsters, monsterID)
log.Printf("怪物 %s 被击杀", monsterID)
}
// 穿透子弹命中后继续穿透直到穿透次数用完
// 非穿透子弹命中后直接结束
if !bullet.Penetration {
break
} else {
bullet.RemainingPenetrations--
log.Printf("子弹 %s 剩余穿透次数: %d", bullet.ID, bullet.RemainingPenetrations)
// 穿透次数用完,子弹消失
if bullet.RemainingPenetrations <= 0 {
bullet.Active = false
toRemove = append(toRemove, id)
break
}
}
}
}
// 穿透子弹没有射程限制,非穿透子弹检查射程
// 注意:穿透子弹的穿透次数检查在碰撞循环内完成
}
// 删除无效子弹
for _, id := range toRemove {
delete(g.bullets, id)
}
}
// updateAreaEffects 更新所有范围效果
func (g *Game) updateAreaEffects() {
// 记录需要删除的效果
toRemove := make([]string, 0)
for id, effect := range g.areaEffects {
if !effect.Active {
toRemove = append(toRemove, id)
continue
}
// 更新已过时间
effect.Elapsed += g.config.UpdateInterval
// 检查是否过期
if effect.Elapsed >= effect.Duration {
effect.Active = false
toRemove = append(toRemove, id)
continue
}
// 检测范围内怪物并造成伤害
if effect.Elapsed-effect.LastTick >= effect.TickInterval {
// 获取对应的效果策略
strategy := GetEffectStrategy(effect.Type)
// 遍历所有怪物
for _, monster := range g.monsters {
distance := effect.Position.Distance(monster.Position)
if distance <= effect.Radius {
// 冰冻效果特殊处理:减速
if effect.Type == EffectTypeIce {
// 只有当怪物当前没有减速效果或减速效果即将过期时才更新
if monster.SpeedReduce <= 0 || time.Now().After(monster.SpeedReduceEnd) {
// 应用减速效果
monster.SpeedReduce = effect.SpeedReduce
monster.CurrentSpeed = monster.Speed * (1 - effect.SpeedReduce)
monster.SpeedReduceEnd = time.Now().Add(effect.Duration)
log.Printf("怪物 %s 被减速 %.0f%%,当前速度: %.2f",
monster.ID, effect.SpeedReduce*100, monster.CurrentSpeed)
}
} else if effect.Type == EffectTypeStun {
// 眩晕效果:怪物定身
if !monster.Stunned || time.Now().After(monster.StunnedEnd) {
monster.Stunned = true
monster.StunnedEnd = time.Now().Add(effect.Duration)
log.Printf("怪物 %s 被眩晕,持续时间: %v", monster.ID, effect.Duration)
}
} else if effect.Type == EffectTypeBurn {
// 燃烧效果:持续挂载范围内的怪物
if !monster.Burning {
monster.Burning = true
monster.BurnEnd = time.Now().Add(effect.Duration)
monster.BurnDamage = effect.Damage
log.Printf("怪物 %s 被挂载燃烧效果,每秒伤害: %d", monster.ID, effect.Damage)
}
} else {
// 其他效果(火焰、毒)造成伤害
isDead := strategy.OnTick(effect, monster, g)
if isDead {
log.Printf("怪物 %s 被 %s 效果击杀", monster.ID, strategy.GetName())
delete(g.monsters, monster.ID)
}
}
}
}
effect.LastTick = effect.Elapsed
}
}
// 删除无效效果
for _, id := range toRemove {
delete(g.areaEffects, id)
}
}
// moveMonster 移动怪物向玩家
func (g *Game) moveMonster(monster *Monster) {
now := time.Now()
// 检查眩晕状态
if monster.Stunned && now.Before(monster.StunnedEnd) {
// 怪物被眩晕,无法移动
return
}
// 眩晕效果结束
if monster.Stunned && now.After(monster.StunnedEnd) {
monster.Stunned = false
log.Printf("怪物 %s 眩晕效果结束", monster.ID)
}
// 检查燃烧状态
if monster.Burning && now.Before(monster.BurnEnd) {
// 燃烧伤害(每秒扣除)
monster.HP -= monster.BurnDamage
if monster.HP <= 0 {
log.Printf("怪物 %s 燃烧伤害死亡,剩余血量: %d", monster.ID, monster.HP)
return
}
}
// 燃烧效果结束
if monster.Burning && now.After(monster.BurnEnd) {
monster.Burning = false
monster.BurnDamage = 0
log.Printf("怪物 %s 燃烧效果结束", monster.ID)
}
// 更新当前速度(检查减速效果是否过期)
if monster.SpeedReduce > 0 && now.After(monster.SpeedReduceEnd) {
// 减速效果结束,恢复原始速度
monster.SpeedReduce = 0
monster.CurrentSpeed = monster.Speed
log.Printf("怪物 %s 减速效果结束", monster.ID)
}
// 计算方向
dx := g.player.Position.X - monster.Position.X
dy := g.player.Position.Y - monster.Position.Y
distance := math.Sqrt(dx*dx + dy*dy)
if distance > 0 {
// 标准化方向向量
dx /= distance
dy /= distance
// 使用当前速度移动
monster.Position.X += dx * monster.CurrentSpeed
monster.Position.Y += dy * monster.CurrentSpeed
}
}
// checkCombat 检测战斗
func (g *Game) checkCombat() {
// 玩家攻击怪物
for id, monster := range g.monsters {
distance := g.player.Position.Distance(monster.Position)
if distance <= g.config.AttackRange {
monster.HP -= g.player.Attack
log.Printf("monster %s HP: %d", id, monster.HP)
if monster.HP <= 0 {
log.Printf("monster %s is defeated", id)
delete(g.monsters, id)
}
}
}
// 怪物攻击玩家
for _, monster := range g.monsters {
distance := g.player.Position.Distance(monster.Position)
if distance <= g.config.AttackRange {
g.player.HP -= 5 // 怪物基础伤害
log.Printf("player %s HP: %d", g.player.ID, g.player.HP)
if g.player.HP <= 0 {
log.Printf("player %s is defeated", g.player.ID)
g.gameOver = true
}
}
}
}
// IsGameOver 游戏是否结束
func (g *Game) IsGameOver() bool {
g.mu.RLock()
defer g.mu.RUnlock()
return g.gameOver
}
// GetConfig 获取游戏配置
func (g *Game) GetConfig() GameConfig {
return g.config
}
测试
package game
import (
"math"
"testing"
"time"
)
func TestNewGame(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 1 * time.Second,
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 100,
}
game := NewGame(config)
if game.GetPlayer().Position.X != config.MapWidth/2 || game.GetPlayer().Position.Y != config.MapHeight/2 {
t.Errorf("玩家初始位置不正确,期望(%f, %f),实际(%f, %f)",
config.MapWidth/2, config.MapHeight/2,
game.GetPlayer().Position.X, game.GetPlayer().Position.Y)
}
if game.GetPlayer().HP != config.PlayerStartHP {
t.Errorf("玩家初始血量不正确,期望%d,实际%d", config.PlayerStartHP, game.GetPlayer().HP)
}
}
func TestMonsterSpawning(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 100,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(350 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) < 3 {
t.Errorf("怪物生成数量不足,期望至少3个,实际%d个", len(monsters))
}
// 检查怪物类型
for _, monster := range monsters {
switch monster.Type {
case MonsterTypeSlime, MonsterTypeGoblin, MonsterTypeOrc, MonsterTypeDragon:
// 正常类型
default:
t.Errorf("未知怪物类型: %s", monster.Type)
}
}
}
func TestMonsterMovement(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000, // 高血量防止死亡
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(150 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 记录初始位置
initialPos := monsters[0].Position
// 更新游戏状态
game.Update()
// 检查怪物是否移动
newPos := monsters[0].Position
if initialPos.X == newPos.X && initialPos.Y == newPos.Y {
t.Error("怪物没有移动")
}
}
func TestCombat(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 50 * time.Millisecond, // 更快生成怪物
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成更多怪物
time.Sleep(300 * time.Millisecond)
// 获取怪物数量
initialMonsterCount := len(game.GetMonsters())
t.Logf("怪物数量: %d", initialMonsterCount)
if initialMonsterCount == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置怪物速度为快速
g := game.GetMonsters()
for _, m := range g {
m.Speed = 100.0
}
// 多次更新让怪物靠近玩家并战斗
for i := 0; i < 1000; i++ {
game.Update()
}
// 检查怪物是否被击杀
finalMonsterCount := len(game.GetMonsters())
if finalMonsterCount >= initialMonsterCount {
t.Errorf("怪物没有被击杀,初始%d个,最终%d个", initialMonsterCount, finalMonsterCount)
}
}
func TestGameOver(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 50 * time.Millisecond,
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 10,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成多个怪物
time.Sleep(200 * time.Millisecond)
// 设置怪物速度为快速
g := game.GetMonsters()
if len(g) < 2 {
t.Skip("怪物数量不足,跳过测试")
}
for _, m := range g {
m.Speed = 100.0
}
// 更新直到游戏结束
for i := 0; i < 50; i++ {
game.Update()
if game.IsGameOver() {
break
}
}
if !game.IsGameOver() {
t.Error("游戏应该结束但没有结束")
}
if game.GetPlayer().HP > 0 {
t.Errorf("玩家应该死亡,当前血量%d", game.GetPlayer().HP)
}
}
func TestAutoShoot(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: 200, // 设置自动射击范围
AutoShootInterval: 200 * time.Millisecond, // 设置自动射击间隔
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 获取一个怪物并设置它在自动射击范围内(较近位置,让子弹更快到达)
monster := monsters[0]
playerPos := game.GetPlayer().Position
monster.Position = Position{X: playerPos.X + 50, Y: playerPos.Y} // 在200范围内,距离50
monster.Speed = 0 // 禁止移动
monster.HP = 50
t.Logf("玩家位置: (%f, %f)", playerPos.X, playerPos.Y)
t.Logf("怪物位置: (%f, %f), 血量: %d", monster.Position.X, monster.Position.Y, monster.HP)
// 等待自动射击并更新游戏状态
time.Sleep(500 * time.Millisecond)
// 额外调用几次更新确保子弹能到达目标
for i := 0; i < 10; i++ {
game.Update()
}
// 检查怪物是否被自动射击击中
if monster.HP >= 50 {
t.Error("怪物没有被自动射击击中")
} else {
t.Logf("怪物被自动射击击中,剩余血量: %d", monster.HP)
}
}
func TestShooting(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 获取一个怪物并设置它在玩家正前方
monster := monsters[0]
playerPos := game.GetPlayer().Position
monster.Position = Position{X: playerPos.X + 100, Y: playerPos.Y}
monster.Speed = 0 // 禁止移动
monster.HP = 50
t.Logf("玩家位置: (%f, %f)", playerPos.X, playerPos.Y)
t.Logf("怪物位置: (%f, %f), 血量: %d", monster.Position.X, monster.Position.Y, monster.HP)
// 向怪物方向射击
game.Shoot(monster.Position.X, monster.Position.Y)
// 更新游戏状态让子弹飞行
for i := 0; i < 20; i++ {
game.Update()
}
// 检查怪物是否被击中
if monster.HP >= 50 {
t.Error("怪物没有被击中")
} else {
t.Logf("怪物被击中,剩余血量: %d", monster.HP)
}
}
func TestFireEffect(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 获取一个怪物并设置它在火海范围内
monster := monsters[0]
monster.Position = Position{X: 500, Y: 500} // 火海中心
monster.Speed = 0 // 禁止移动
monster.HP = 50
t.Logf("怪物位置: (%f, %f), 血量: %d", monster.Position.X, monster.Position.Y, monster.HP)
// 创建火海(持续2秒,范围100,每500ms造成5点伤害)
game.CreateFireEffect(500, 500, 100, 5, 2*time.Second)
// 等待火海造成伤害(等待4次伤害tick)
time.Sleep(2500 * time.Millisecond)
// 检查怪物是否被火海击杀
if monster.HP > 0 {
t.Errorf("火海应该击杀怪物,当前血量: %d", monster.HP)
} else {
t.Logf("火海测试完成,怪物被击杀")
}
}
func TestPoisonEffect(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置多个怪物在毒池范围内
playerPos := game.GetPlayer().Position
for i, monster := range monsters {
monster.Position = Position{X: playerPos.X + float64(i)*20, Y: playerPos.Y}
monster.Speed = 0
monster.HP = 30
}
// 创建毒池
game.CreatePoisonEffect(playerPos.X, playerPos.Y, 80, 3, 1*time.Second)
// 等待毒池造成伤害
time.Sleep(1500 * time.Millisecond)
// 检查有多少怪物被毒击杀
deadCount := 0
for _, monster := range monsters {
if monster.HP <= 0 {
deadCount++
}
}
t.Logf("毒池测试完成,被击杀怪物数量: %d/%d", deadCount, len(monsters))
if deadCount == 0 {
t.Error("应该有怪物被毒池击杀")
}
}
func TestAreaEffectDuration(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 创建一个持续1秒的火海
game.CreateFireEffect(500, 500, 50, 10, 1*time.Second)
// 检查效果是否已创建
if len(game.areaEffects) != 1 {
t.Errorf("应该有1个活动效果,当前: %d", len(game.areaEffects))
}
// 等待效果结束
time.Sleep(1500 * time.Millisecond)
// 检查效果是否已自动移除
if len(game.areaEffects) != 0 {
t.Errorf("效果应该已自动移除,当前: %d", len(game.areaEffects))
} else {
t.Log("范围效果持续时间测试通过")
}
}
func TestIceEffect(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置一个怪物在冰冻范围内
monster := monsters[0]
monster.Position = Position{X: 500, Y: 500}
monster.HP = 1000 // 设置高HP确保怪物不会在测试期间死亡
originalSpeed := monster.Speed
t.Logf("怪物原始速度: %.2f", originalSpeed)
// 创建冰冻效果(减速50%)
game.CreateIceEffect(500, 500, 100, 0.5, 1*time.Second)
// 等待减速效果生效
time.Sleep(200 * time.Millisecond)
// 检查怪物速度是否降低
if monster.CurrentSpeed >= originalSpeed {
t.Errorf("怪物速度应该降低,当前速度: %.2f,原始速度: %.2f", monster.CurrentSpeed, originalSpeed)
} else {
t.Logf("冰冻效果生效,怪物速度从 %.2f 降至 %.2f", originalSpeed, monster.CurrentSpeed)
}
// 等待减速效果结束
time.Sleep(1200 * time.Millisecond)
// 检查速度是否恢复
if monster.CurrentSpeed != originalSpeed {
t.Errorf("怪物速度应该恢复,当前: %.2f,原始: %.2f", monster.CurrentSpeed, originalSpeed)
} else {
t.Log("减速效果结束,怪物速度已恢复")
}
}
func TestBulletKnockback(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
// 获取怪物ID
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
monsterID := monsters[0].ID
// 切换为狙击枪(有更大击退效果)
game.GetPlayer().Weapon = NewWeapon(WeaponTypeSniper, 1)
// 设置怪物位置(通过游戏内部方法)
game.mu.Lock()
if monster, ok := game.monsters[monsterID]; ok {
monster.Position = Position{X: 700, Y: 500}
monster.HP = 200
monster.Speed = 0
monster.CurrentSpeed = 0
monster.Stunned = true // 眩晕怪物,防止任何移动
monster.StunnedEnd = time.Now().Add(10 * time.Second)
}
originalPos := game.monsters[monsterID].Position
t.Logf("怪物原始位置: (%.2f, %.2f)", originalPos.X, originalPos.Y)
game.mu.Unlock()
// 射击怪物
game.Shoot(700, 500)
// 等待子弹飞行(狙击枪子弹速度快,增加等待时间)
time.Sleep(500 * time.Millisecond)
// 检查怪物是否被击退(线程安全方式)
game.mu.Lock()
monster := game.monsters[monsterID]
knockbackDistance := math.Sqrt(math.Pow(monster.Position.X-originalPos.X, 2) + math.Pow(monster.Position.Y-originalPos.Y, 2))
t.Logf("怪物被击退距离: %.2f,新位置: (%.2f, %.2f)", knockbackDistance, monster.Position.X, monster.Position.Y)
game.mu.Unlock()
// 狙击枪击退力为50,应该有明显的击退效果
if knockbackDistance < 30 {
t.Errorf("狙击枪击退效果不足,实际距离: %.2f", knockbackDistance)
} else {
t.Log("狙击枪击退效果测试通过")
}
}
func TestShotgun(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
// 切换为xian弹枪
game.GetPlayer().Weapon = NewWeapon(WeaponTypeShotgun, 1)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(300 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置多个怪物在xian弹枪范围内
playerPos := game.GetPlayer().Position
for i, monster := range monsters {
// 将怪物排列成扇形
angle := (float64(i) - float64(len(monsters)-1)/2) * 0.2
monster.Position = Position{
X: playerPos.X + 80*math.Cos(angle),
Y: playerPos.Y + 80*math.Sin(angle),
}
monster.Speed = 0
monster.HP = 50
t.Logf("怪物%d位置: (%f, %f)", i, monster.Position.X, monster.Position.Y)
}
// 向中间怪物射击
if len(monsters) > 0 {
targetMonster := monsters[len(monsters)/2]
game.Shoot(targetMonster.Position.X, targetMonster.Position.Y)
}
// 等待子弹命中
for i := 0; i < 10; i++ {
game.Update()
}
// 检查有多少怪物被击中
hitCount := 0
for _, monster := range monsters {
if monster.HP < 50 {
hitCount++
t.Logf("怪物被击中,剩余血量: %d", monster.HP)
}
}
t.Logf("xian弹枪测试完成,击中怪物数量: %d/%d", hitCount, len(monsters))
if hitCount == 0 {
t.Error("没有怪物被xian弹枪击中")
}
}
func TestRifle(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
// 切换为步枪(带穿透)
game.GetPlayer().Weapon = NewWeapon(WeaponTypeRifle, 2)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(300 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) < 2 {
t.Skip("怪物数量不足,跳过测试")
}
// 设置怪物排成一条直线
playerPos := game.GetPlayer().Position
monsters[0].Position = Position{X: playerPos.X + 100, Y: playerPos.Y}
monsters[1].Position = Position{X: playerPos.X + 250, Y: playerPos.Y}
monsters[0].Speed = 0
monsters[1].Speed = 0
monsters[0].HP = 50
monsters[1].HP = 50
t.Logf("怪物1位置: (%f, %f)", monsters[0].Position.X, monsters[0].Position.Y)
t.Logf("怪物2位置: (%f, %f)", monsters[1].Position.X, monsters[1].Position.Y)
// 向远处怪物射击
game.Shoot(monsters[1].Position.X, monsters[1].Position.Y)
// 等待子弹穿透
for i := 0; i < 20; i++ {
game.Update()
}
// 检查两个怪物是否都被击中
if monsters[0].HP >= 50 {
t.Error("怪物1没有被步枪穿透击中")
}
if monsters[1].HP >= 50 {
t.Error("怪物2没有被步枪击中")
}
t.Logf("步枪测试完成 - 怪物1剩余血量: %d, 怪物2剩余血量: %d", monsters[0].HP, monsters[1].HP)
}
func TestSniper(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
// 切换为高等级狙击枪
game.GetPlayer().Weapon = NewWeapon(WeaponTypeSniper, 3)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置怪物在远距离
monster := monsters[0]
playerPos := game.GetPlayer().Position
monster.Position = Position{X: playerPos.X + 400, Y: playerPos.Y} // 远距离
monster.Speed = 0
monster.HP = 100
t.Logf("怪物位置: (%f, %f), 初始血量: %d", monster.Position.X, monster.Position.Y, monster.HP)
// 狙击枪射击
game.Shoot(monster.Position.X, monster.Position.Y)
// 等待子弹飞行
for i := 0; i < 30; i++ {
game.Update()
}
// 检查高伤害
weapon := game.GetPlayer().Weapon
expectedDamage := int(float64(weapon.Damage) * weapon.DamageMultiplier)
t.Logf("狙击枪伤害: 基础%d × 倍率%.2f = %d", weapon.Damage, weapon.DamageMultiplier, expectedDamage)
if monster.HP > 100-expectedDamage {
t.Errorf("狙击枪伤害不足,期望至少%d,实际造成%d", expectedDamage, 100-monster.HP)
} else {
t.Logf("狙击枪测试完成,怪物剩余血量: %d", monster.HP)
}
}
func TestWeaponLevel(t *testing.T) {
// 测试不同等级武器的伤害倍率
weapon1 := NewWeapon(WeaponTypePistol, 1)
weapon5 := NewWeapon(WeaponTypePistol, 5)
t.Logf("1级手枪: 伤害%d, 倍率%.2f, 最终伤害%d",
weapon1.Damage, weapon1.DamageMultiplier,
int(float64(weapon1.Damage)*weapon1.DamageMultiplier))
t.Logf("5级手枪: 伤害%d, 倍率%.2f, 最终伤害%d",
weapon5.Damage, weapon5.DamageMultiplier,
int(float64(weapon5.Damage)*weapon5.DamageMultiplier))
// 验证伤害倍率计算正确
expectedMultiplier := 1.0 + float64(5-1)*0.2 // 1.8
if math.Abs(weapon5.DamageMultiplier-expectedMultiplier) > 0.01 {
t.Errorf("5级武器伤害倍率计算错误,期望%.2f,实际%.2f", expectedMultiplier, weapon5.DamageMultiplier)
}
}
func TestBulletPenetration(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 50 * time.Millisecond, // 更快生成
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(300 * time.Millisecond) // 更长等待时间
monsters := game.GetMonsters()
if len(monsters) < 2 {
t.Skip("怪物数量不足,跳过测试")
}
// 获取玩家位置
playerPos := game.GetPlayer().Position
// 设置两个怪物在同一条线上
monster1 := monsters[0]
monster2 := monsters[1]
monster1.Position = Position{X: playerPos.X + 50, Y: playerPos.Y}
monster2.Position = Position{X: playerPos.X + 150, Y: playerPos.Y}
monster1.Speed = 0
monster2.Speed = 0
monster1.HP = 20
monster2.HP = 20
t.Logf("怪物1位置: (%f, %f), 血量: %d", monster1.Position.X, monster1.Position.Y, monster1.HP)
t.Logf("怪物2位置: (%f, %f), 血量: %d", monster2.Position.X, monster2.Position.Y, monster2.HP)
// 设置武器穿透次数为2
game.GetPlayer().Weapon.MaxPenetration = 2
// 开启穿透功能
game.ToggleWeaponPenetration()
// 向怪物方向射击
game.Shoot(monster2.Position.X, monster2.Position.Y)
// 更新游戏状态让子弹飞行
for i := 0; i < 30; i++ {
game.Update()
}
// 检查两个怪物是否都被击中
if monster1.HP >= 20 {
t.Error("怪物1没有被穿透子弹击中")
}
if monster2.HP >= 20 {
t.Error("怪物2没有被击中")
}
t.Logf("穿透测试完成 - 怪物1剩余血量: %d, 怪物2剩余血量: %d", monster1.HP, monster2.HP)
}
func TestMonsterFollowsPlayer(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 100 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(150 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
// 设置怪物速度为慢速,方便观察
for _, m := range monsters {
m.Speed = 5.0
}
// 设置怪物初始位置在玩家上方(Y坐标小于玩家)
game.mu.Lock()
monsterID := monsters[0].ID
if monster, ok := game.monsters[monsterID]; ok {
monster.Position = Position{X: 500, Y: 300} // 在玩家上方
}
monster := game.monsters[monsterID]
initialMonsterPos := monster.Position
initialPlayerPos := game.player.Position
game.mu.Unlock()
t.Logf("初始状态 - 玩家位置: (%f, %f), 怪物位置: (%f, %f)",
initialPlayerPos.X, initialPlayerPos.Y,
initialMonsterPos.X, initialMonsterPos.Y)
// 更新几次让怪物开始移动
for i := 0; i < 5; i++ {
game.Update()
}
// 记录怪物向初始位置移动后的位置
game.mu.Lock()
posAfterFirstMove := game.monsters[monsterID].Position
game.mu.Unlock()
t.Logf("怪物向初始玩家位置移动后: (%f, %f)", posAfterFirstMove.X, posAfterFirstMove.Y)
// 玩家移动到新位置
game.MovePlayer(100, 100)
newPlayerPos := game.GetPlayer().Position
t.Logf("玩家移动到新位置: (%f, %f)", newPlayerPos.X, newPlayerPos.Y)
// 更新几次让怪物改变方向
for i := 0; i < 5; i++ {
game.Update()
}
// 检查怪物是否改变了移动方向(朝向新的玩家位置)
game.mu.Lock()
posAfterPlayerMove := game.monsters[monsterID].Position
game.mu.Unlock()
t.Logf("怪物向新玩家位置移动后: (%f, %f)", posAfterPlayerMove.X, posAfterPlayerMove.Y)
// 计算怪物移动方向
// 如果玩家向右移动,怪物应该也向右移动更多
if newPlayerPos.X > initialPlayerPos.X {
if posAfterPlayerMove.X <= posAfterFirstMove.X {
t.Error("怪物应该跟随玩家向右移动")
}
}
if newPlayerPos.Y > initialPlayerPos.Y {
if posAfterPlayerMove.Y <= posAfterFirstMove.Y {
t.Error("怪物应该跟随玩家向下移动")
}
}
t.Log("怪物成功跟随玩家移动!")
}
func TestPositionDistance(t *testing.T) {
p1 := Position{X: 0, Y: 0}
p2 := Position{X: 3, Y: 4}
distance := p1.Distance(p2)
if distance != 5 {
t.Errorf("距离计算错误,期望5,实际%f", distance)
}
}
func TestMonsterHP(t *testing.T) {
tests := []struct {
name string
monster MonsterType
expected int
}{
{"史莱姆", MonsterTypeSlime, 20},
{"哥布林", MonsterTypeGoblin, 40},
{"兽人", MonsterTypeOrc, 80},
{"龙", MonsterTypeDragon, 200},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hp := getMonsterHP(tt.monster)
if hp != tt.expected {
t.Errorf("%s血量错误,期望%d,实际%d", tt.name, tt.expected, hp)
}
})
}
}
// TestStunEffect 测试眩晕效果
func TestStunEffect(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
monsterID := monsters[0].ID
// 设置怪物位置在玩家附近,并立即眩晕(线程安全方式)
game.mu.Lock()
var initialPos Position
if monster, ok := game.monsters[monsterID]; ok {
monster.Position = Position{X: 550, Y: 500}
monster.HP = 200
// 立即设置眩晕状态
monster.Stunned = true
monster.StunnedEnd = time.Now().Add(2 * time.Second)
initialPos = monster.Position
}
game.mu.Unlock()
// 等待一段时间,让游戏更新
time.Sleep(300 * time.Millisecond)
// 更新游戏状态
game.Update()
// 检查怪物是否被眩晕(应该没有移动)
game.mu.Lock()
monster := game.monsters[monsterID]
newPos := monster.Position
isStunned := monster.Stunned
game.mu.Unlock()
if !isStunned {
t.Error("怪物应该被眩晕")
}
// 检查怪物位置是否变化(眩晕期间应该不移动)
if math.Abs(newPos.X-initialPos.X) > 1 || math.Abs(newPos.Y-initialPos.Y) > 1 {
t.Errorf("眩晕期间怪物不应该移动,初始位置: (%.2f, %.2f),新位置: (%.2f, %.2f)",
initialPos.X, initialPos.Y, newPos.X, newPos.Y)
}
t.Log("眩晕效果测试通过")
}
// TestBurnEffect 测试燃烧效果
func TestBurnEffect(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
monsterID := monsters[0].ID
// 设置怪物位置和血量
game.mu.Lock()
if monster, ok := game.monsters[monsterID]; ok {
monster.Position = Position{X: 600, Y: 500}
monster.HP = 100
}
initialHP := game.monsters[monsterID].HP
game.mu.Unlock()
t.Logf("怪物初始血量: %d", initialHP)
// 创建燃烧效果(每秒伤害10点,持续2秒)
game.CreateBurnEffect(600, 500, 80, 10, 2*time.Second)
// 等待燃烧效果生效
time.Sleep(600 * time.Millisecond)
// 检查怪物是否被燃烧
game.mu.Lock()
monster := game.monsters[monsterID]
isBurning := monster.Burning
currentHP := monster.HP
game.mu.Unlock()
if !isBurning {
t.Error("怪物应该被燃烧")
}
// 检查燃烧伤害(应该至少扣除一次伤害)
if currentHP >= initialHP {
t.Errorf("燃烧应该造成伤害,初始血量: %d,当前血量: %d", initialHP, currentHP)
}
t.Logf("燃烧效果测试通过,怪物剩余血量: %d", currentHP)
}
// TestCriticalHit 测试暴击系统
func TestCriticalHit(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 等待生成怪物
time.Sleep(200 * time.Millisecond)
monsters := game.GetMonsters()
if len(monsters) == 0 {
t.Skip("没有生成怪物,跳过测试")
}
monsterID := monsters[0].ID
// 设置怪物位置和高血量
game.mu.Lock()
if monster, ok := game.monsters[monsterID]; ok {
monster.Position = Position{X: 700, Y: 500}
monster.HP = 500
monster.Stunned = true
monster.StunnedEnd = time.Now().Add(10 * time.Second)
}
initialHP := game.monsters[monsterID].HP
game.mu.Unlock()
t.Logf("怪物初始血量: %d", initialHP)
// 切换为狙击枪(高暴击率30%)
game.GetPlayer().Weapon = NewWeapon(WeaponTypeSniper, 1)
// 多次射击,期望至少出现一次暴击
totalShoots := 20
for i := 0; i < totalShoots; i++ {
game.Shoot(700, 500)
time.Sleep(100 * time.Millisecond)
}
// 等待子弹命中
time.Sleep(500 * time.Millisecond)
// 检查怪物剩余血量
game.mu.Lock()
monster := game.monsters[monsterID]
currentHP := monster.HP
game.mu.Unlock()
// 计算平均伤害
baseDamage := 50 // 狙击枪基础伤害
expectedMaxNormalDamage := baseDamage * totalShoots
actualDamage := initialHP - currentHP
t.Logf("总伤害: %d,期望最大普通伤害: %d", actualDamage, expectedMaxNormalDamage)
// 如果总伤害超过最大普通伤害,说明发生了暴击
if actualDamage > expectedMaxNormalDamage {
t.Log("暴击系统测试通过 - 检测到暴击")
} else {
// 20次射击30%暴击率,不暴击概率约为0.7^20 ≈ 0.0008,很小概率事件
t.Logf("未检测到暴击(概率极低),总伤害: %d,期望最大普通伤害: %d", actualDamage, expectedMaxNormalDamage)
}
}
// TestAmmoSystem 测试弹药系统
func TestAmmoSystem(t *testing.T) {
config := GameConfig{
MapWidth: 1000,
MapHeight: 1000,
MonsterSpawnInterval: 100 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
AttackRange: 50,
PlayerStartHP: 1000,
AutoShootRange: -1, // 关闭自动射击
}
game := NewGame(config)
game.Start()
defer game.Stop()
// 切换为手枪(12发弹药,500ms射速冷却)
pistol := NewWeapon(WeaponTypePistol, 1)
game.GetPlayer().Weapon = pistol
// 检查初始弹药
if pistol.CurrentAmmo != pistol.MaxAmmo {
t.Errorf("初始弹药错误,期望%d,实际%d", pistol.MaxAmmo, pistol.CurrentAmmo)
}
// 射击12次,消耗所有弹药(等待射速冷却)
for i := 0; i < 12; i++ {
game.Shoot(500, 300)
time.Sleep(pistol.FireRate + 50*time.Millisecond)
}
// 检查弹药是否耗尽(线程安全方式)
game.mu.Lock()
currentAmmo := pistol.CurrentAmmo
game.mu.Unlock()
if currentAmmo != 0 {
t.Errorf("弹药应该耗尽,当前弹药: %d", currentAmmo)
}
// 再次射击,应该自动换弹
game.Shoot(500, 300)
// 等待一小段时间确保换弹开始
time.Sleep(100 * time.Millisecond)
// 检查是否正在换弹(线程安全方式)
game.mu.Lock()
isReloading := game.reloading[string(pistol.Type)]
game.mu.Unlock()
if !isReloading {
t.Error("应该正在换弹")
}
// 等待换弹完成
time.Sleep(pistol.ReloadTime + 200*time.Millisecond)
// 检查弹药是否恢复
if pistol.CurrentAmmo != pistol.MaxAmmo {
t.Errorf("换弹完成后弹药应该恢复,当前弹药: %d/%d", pistol.CurrentAmmo, pistol.MaxAmmo)
}
t.Log("弹药系统测试通过")
}