173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
// 2D 台球在房间内的动作处理、AI/托管击球与状态快照
|
||
// 每一杆由服务端权威模拟,回放关键帧通过 bill_anim 事件推给双方
|
||
package room
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"nl-game-api-gin/internal/gamecore/billiards"
|
||
)
|
||
|
||
// handleBillShotLocked 真人击球(需持锁):支持杆法(高低杆/左右塞)
|
||
func (r *Room) handleBillShotLocked(seatIdx int, angle, power, spinX, spinY float64, c *Client) {
|
||
if r.status != "playing" || r.billGame == nil {
|
||
c.pushError("对局未开始")
|
||
return
|
||
}
|
||
res, err := r.billGame.ShootSpin(seatIdx, angle, power, spinX, spinY)
|
||
if err != nil {
|
||
c.pushError(err.Error())
|
||
return
|
||
}
|
||
r.afterBillShotLocked(seatIdx, res)
|
||
}
|
||
|
||
// aiBillActLocked AI/托管击球:规则 AI 选线路后带噪声出杆(需持锁)
|
||
// escrow=true 为超时托管:使用低难度(大噪声)出杆
|
||
func (r *Room) aiBillActLocked(seatIdx int, escrow bool) {
|
||
g := r.billGame
|
||
if g == nil || r.status != "playing" || g.Turn != seatIdx {
|
||
return
|
||
}
|
||
diff := r.AIDifficulty
|
||
if escrow {
|
||
diff = "easy"
|
||
}
|
||
angle, power := billiards.AIShot(g, seatIdx, diff)
|
||
res, err := g.Shoot(seatIdx, angle, power)
|
||
if err != nil {
|
||
return
|
||
}
|
||
r.afterBillShotLocked(seatIdx, res)
|
||
}
|
||
|
||
// afterBillShotLocked 一杆结束后的统一收尾:推送回放动画、播报、结算或调度下一杆(需持锁)
|
||
func (r *Room) afterBillShotLocked(seatIdx int, res *billiards.ShotResult) {
|
||
// 回放动画推给所有真人(20fps 关键帧,前端插值播放)
|
||
// 附带犯规标记与原因:前端回放结束后先弹犯规提示,再播换人横幅
|
||
animMs := len(res.Frames) * 50
|
||
foulReason := ""
|
||
if res.Foul {
|
||
foulReason = "空杆,未碰到任何球"
|
||
for _, id := range res.Potted {
|
||
if id == 0 {
|
||
foulReason = "白球落袋"
|
||
break
|
||
}
|
||
}
|
||
}
|
||
anim := map[string]any{
|
||
"shooter": seatIdx, "frames": res.Frames,
|
||
"potted": res.Potted, "interval_ms": 50,
|
||
"foul": res.Foul && !res.Over, "foul_reason": foulReason,
|
||
}
|
||
for _, s := range r.seats {
|
||
if s.client != nil {
|
||
s.client.push("bill_anim", anim)
|
||
}
|
||
}
|
||
r.animMs = animMs
|
||
// 组装战报
|
||
name := r.seats[seatIdx].Name
|
||
var lines []string
|
||
if names := pottedNames(res.Potted); names != "" {
|
||
lines = append(lines, fmt.Sprintf("%s 打进了 %s", name, names))
|
||
}
|
||
if res.Desc != "" {
|
||
lines = append(lines, res.Desc)
|
||
}
|
||
// 打进对方目标球的说明(简化规则:送球不判犯规,只影响是否续杆)
|
||
if !res.Over && oppBallPotted(r.billGame.Groups[seatIdx], res.Potted) {
|
||
lines = append(lines, fmt.Sprintf("%s 把对方的球送进袋(不判犯规)", name))
|
||
}
|
||
if res.Foul && !res.Over {
|
||
lines = append(lines, fmt.Sprintf("%s 犯规,击球权交给对方", name))
|
||
} else if res.Continue && !res.Over {
|
||
lines = append(lines, fmt.Sprintf("%s 打进己方目标球,继续击球", name))
|
||
}
|
||
for _, l := range lines {
|
||
r.broadcastChatLocked(-1, "系统", "", l, false)
|
||
}
|
||
if res.Over {
|
||
// 黑八落袋的胜负结算延迟到回放播完再广播,避免提前剧透结果
|
||
r.turnSeq++ // 使遗留的回合超时定时器失效
|
||
if r.turnTimer != nil {
|
||
r.turnTimer.Stop()
|
||
}
|
||
winners := []int{res.Winner}
|
||
desc := fmt.Sprintf("%s 获胜!", r.seats[res.Winner].Name)
|
||
time.AfterFunc(time.Duration(animMs+600)*time.Millisecond, func() {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
// 等待期间房间可能已被解散,不再结算与广播
|
||
if r.closed {
|
||
return
|
||
}
|
||
r.settleLocked(winners, desc)
|
||
r.broadcastStateLocked()
|
||
})
|
||
} else {
|
||
r.scheduleTurnLocked()
|
||
}
|
||
r.broadcastStateLocked()
|
||
}
|
||
|
||
// oppBallPotted 本杆是否把对方组的球打进袋(分组未定时不判断)
|
||
func oppBallPotted(myGroup int, potted []int) bool {
|
||
if myGroup == billiards.GroupNone {
|
||
return false
|
||
}
|
||
for _, id := range potted {
|
||
var g int
|
||
switch {
|
||
case id >= 1 && id <= 7:
|
||
g = billiards.GroupSolid
|
||
case id >= 9 && id <= 15:
|
||
g = billiards.GroupStripe
|
||
default:
|
||
continue
|
||
}
|
||
if g != myGroup {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// pottedNames 落袋球的可读描述(白球单独说明)
|
||
func pottedNames(potted []int) string {
|
||
var parts []string
|
||
for _, id := range potted {
|
||
if id == 0 {
|
||
parts = append(parts, "白球(犯规)")
|
||
} else if id == 8 {
|
||
parts = append(parts, "黑八")
|
||
} else {
|
||
parts = append(parts, fmt.Sprintf("%d 号球", id))
|
||
}
|
||
}
|
||
return strings.Join(parts, "、")
|
||
}
|
||
|
||
// billStateLocked 台球状态快照(需持锁)
|
||
func (r *Room) billStateLocked(viewSeat int) map[string]any {
|
||
g := r.billGame
|
||
balls := make([]map[string]any, 0, 16)
|
||
for _, b := range g.Balls {
|
||
balls = append(balls, map[string]any{
|
||
"id": b.ID, "x": b.X, "y": b.Y, "on": b.On,
|
||
})
|
||
}
|
||
myGroup := 0
|
||
if viewSeat >= 0 && viewSeat < 2 {
|
||
myGroup = g.Groups[viewSeat]
|
||
}
|
||
return map[string]any{
|
||
"phase": g.Phase, "turn": g.Turn, "groups": g.Groups,
|
||
"my_group": myGroup, "balls": balls, "shots": g.Shots,
|
||
"cleared": []bool{g.GroupCleared(0), g.GroupCleared(1)},
|
||
}
|
||
}
|