201 lines
5.6 KiB
Go
201 lines
5.6 KiB
Go
package logic
|
||
|
||
import (
|
||
"encoding/json"
|
||
"math/rand"
|
||
|
||
"wuziqi-server/characters"
|
||
"wuziqi-server/config"
|
||
"wuziqi-server/core"
|
||
"wuziqi-server/items"
|
||
|
||
"github.com/heroiclabs/nakama-common/runtime"
|
||
)
|
||
|
||
type GameEngine struct {
|
||
CharManager *characters.CharacterManager
|
||
ItemManager *items.ItemManager
|
||
Dispatcher runtime.MatchDispatcher
|
||
Logger runtime.Logger
|
||
Presences map[string]runtime.Presence
|
||
DisconnectedPlayers map[string]*core.Player
|
||
}
|
||
|
||
func NewGameEngine(logger runtime.Logger, dispatcher runtime.MatchDispatcher, charMgr *characters.CharacterManager, itemMgr *items.ItemManager, presences map[string]runtime.Presence, disconnected map[string]*core.Player) *GameEngine {
|
||
return &GameEngine{
|
||
CharManager: charMgr,
|
||
ItemManager: itemMgr,
|
||
Dispatcher: dispatcher,
|
||
Logger: logger,
|
||
Presences: presences,
|
||
DisconnectedPlayers: disconnected,
|
||
}
|
||
}
|
||
|
||
// BroadcastEvent 实现 items.GameLogic
|
||
func (e *GameEngine) BroadcastEvent(event core.GameEvent) {
|
||
core.BroadcastEvent(e.Dispatcher, event)
|
||
}
|
||
|
||
// SendPrivateEvent 实现 items.GameLogic,仅向特定用户发送事件
|
||
func (e *GameEngine) SendPrivateEvent(targetID string, event core.GameEvent) {
|
||
presence, ok := e.Presences[targetID]
|
||
if !ok {
|
||
e.Logger.Warn("SendPrivateEvent: Presence not found for user %s", targetID)
|
||
return
|
||
}
|
||
data, _ := json.Marshal(event)
|
||
e.Logger.Debug("SendPrivateEvent: UserID=%s, Type=%s, Msg=%s, FoundPresence=%v", targetID, event.Type, event.Message, ok)
|
||
// 第三个参数限制为仅包含该玩家的 presence 列表
|
||
e.Dispatcher.BroadcastMessage(core.OpCodeGameEvent, data, []runtime.Presence{presence}, nil, true)
|
||
}
|
||
|
||
// GetRandomAliveTarget 实现 items.GameLogic
|
||
func (e *GameEngine) GetRandomAliveTarget(state *core.GameState, excludeID string) *core.Player {
|
||
candidates := []*core.Player{}
|
||
for _, p := range state.Players {
|
||
if p.UserID != excludeID && p.HP > 0 {
|
||
candidates = append(candidates, p)
|
||
}
|
||
}
|
||
if len(candidates) == 0 {
|
||
return nil
|
||
}
|
||
return candidates[rand.Intn(len(candidates))]
|
||
}
|
||
|
||
// CheckGameOver 检查游戏是否结束
|
||
func (e *GameEngine) CheckGameOver(state *core.GameState) bool {
|
||
if !state.GameStarted {
|
||
return false
|
||
}
|
||
|
||
alive := []string{}
|
||
for _, p := range state.Players {
|
||
if p.HP > 0 {
|
||
alive = append(alive, p.UserID)
|
||
}
|
||
}
|
||
// 关键修复:计入正在断线重连中的幸存玩家
|
||
for _, p := range e.DisconnectedPlayers {
|
||
if p.HP > 0 {
|
||
alive = append(alive, p.UserID)
|
||
}
|
||
}
|
||
|
||
if len(alive) <= 1 {
|
||
winnerID := ""
|
||
if len(alive) == 1 {
|
||
winnerID = alive[0]
|
||
} else if len(alive) == 0 {
|
||
winnerID = state.LastDeadPlayerID
|
||
}
|
||
state.WinnerID = winnerID
|
||
state.GameStarted = false
|
||
|
||
// 使用真实用户ID向后端结算游戏
|
||
if winnerID != "" && winnerID != "draw" {
|
||
winnerPlayer, ok := state.Players[winnerID]
|
||
if !ok {
|
||
// 如果在 Players 里没找到,尝试在断线列表中找(可能获胜者刚好断线)
|
||
winnerPlayer = e.DisconnectedPlayers[winnerID]
|
||
}
|
||
|
||
if winnerPlayer != nil && winnerPlayer.RealUserID > 0 {
|
||
// 分数参数暂时传宝箱数,后端奖励由配置决定
|
||
config.SettleGameWithBackend(e.Logger, winnerPlayer.RealUserID, winnerPlayer.Ticket, "", true, winnerPlayer.ChestCount)
|
||
} else {
|
||
e.Logger.Error("Winner player %s has no RealUserID, cannot settle", winnerID)
|
||
}
|
||
}
|
||
|
||
endDataBytes, _ := json.Marshal(map[string]interface{}{
|
||
"winnerId": winnerID,
|
||
"gameState": state.Sanitize(),
|
||
})
|
||
e.Dispatcher.BroadcastMessage(core.OpCodeGameOver, endDataBytes, nil, nil, true)
|
||
// 注意:BroadcastMessage 期望 []byte。
|
||
// 我们应该在内部进行序列化。
|
||
// 等等,Nakama 的 BroadcastMessage 接受 []byte 数据。
|
||
// 我马上修好它。
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// AdvanceTurn 处理回合推进、定时炸弹、毒药等。
|
||
func (e *GameEngine) AdvanceTurn(state *core.GameState) {
|
||
scanCount := 0
|
||
|
||
for {
|
||
state.CurrentTurnIndex = (state.CurrentTurnIndex + 1) % len(state.TurnOrder)
|
||
scanCount++
|
||
|
||
// 回合计数:当回到第一个玩家时,全场轮次+1
|
||
if state.CurrentTurnIndex == 0 {
|
||
state.Round++
|
||
}
|
||
|
||
// 如果每个人都跳过/死亡,防止死循环
|
||
if scanCount > len(state.TurnOrder)*2 {
|
||
break
|
||
}
|
||
|
||
nextUID := state.TurnOrder[state.CurrentTurnIndex]
|
||
nextPlayer := state.Players[nextUID]
|
||
|
||
if nextPlayer == nil {
|
||
e.Logger.Warn("Player %s found in TurnOrder but missing from Players map", nextUID)
|
||
continue
|
||
}
|
||
|
||
if nextPlayer.HP <= 0 {
|
||
continue
|
||
}
|
||
|
||
// 处理定时炸弹倒计时
|
||
if nextPlayer.TimeBombTurns > 0 {
|
||
nextPlayer.TimeBombTurns--
|
||
if nextPlayer.TimeBombTurns == 0 {
|
||
// 轰!定时炸弹爆炸
|
||
// 树懒受到的炸弹伤害减免是在 ApplyDamage 中处理的吗?
|
||
// 原始代码在这里处理:如果是树懒dmg=1,否则dmg=2
|
||
dmg := 2
|
||
if nextPlayer.Character == "sloth" {
|
||
dmg = 1
|
||
}
|
||
e.Logger.Info("Time bomb exploded on player %s! Taking %d damage", nextPlayer.UserID, dmg)
|
||
// ApplyDamage(isItemEffect=true 因为它来自道具?)
|
||
// 是的,BombTimer 是一个道具。
|
||
e.ApplyDamage(state, nextPlayer, dmg, true)
|
||
|
||
if nextPlayer.HP <= 0 {
|
||
continue // 死于炸弹,跳过回合
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理毒药
|
||
if nextPlayer.Poisoned {
|
||
nextPlayer.PoisonSteps++
|
||
if nextPlayer.PoisonSteps%2 == 0 {
|
||
// 毒药伤害
|
||
e.ApplyDamage(state, nextPlayer, 1, true)
|
||
if nextPlayer.HP <= 0 {
|
||
continue // 死于毒药,跳过回合
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理跳过
|
||
if nextPlayer.SkipTurn {
|
||
nextPlayer.SkipTurn = false
|
||
e.Logger.Info("Player %s skipped turn", nextPlayer.UserID)
|
||
continue
|
||
}
|
||
|
||
// 找到有效玩家
|
||
break
|
||
}
|
||
}
|