Files
GaCy-GO/internal/gameserver/handlers_challenge_scene.go
2026-10-28 18:54:43 +07:00

241 lines
8.8 KiB
Go

package gameserver
import (
"errors"
"fmt"
"google.golang.org/protobuf/proto"
"gac-hkrpg-go/internal/common"
goproto "gac-hkrpg-go/protocol/proto"
)
// ---------------------------------------------------------------------------
// tierce lineup store + challenge scene loading
// ---------------------------------------------------------------------------
// loadChallengeScene is the Go port of scene.rs load_challenge_scene.
func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID uint32, avatarIDs []uint32) (*goproto.SceneInfo, *goproto.MotionInfo, bool, error) {
player := s.JSON()
if player == nil {
return nil, nil, false, errors.New("data is not set!")
}
res, err := common.GameRes()
if err != nil {
return nil, nil, false, err
}
name, scene, ok := firstSceneOfEntry(res, entryID)
if !ok {
return nil, nil, false, fmt.Errorf("Challenge Map Entrance Not Found %d", entryID)
}
planeID, floorID, err := parsePlaneFloor(name)
if err != nil {
return nil, nil, false, err
}
// Use the selected group's resource placement, with no guessed arena
// height or offset towards world origin (arena floors need not be there).
group, ok := scene.Scenes[targetGroupID]
if !ok || len(group.Monsters) == 0 {
return nil, nil, false, fmt.Errorf("challenge entrance %d group %d has no monster placement", entryID, targetGroupID)
}
placement := group.Monsters[0]
monsPos := common.Position{X: placement.Pos.X, Y: placement.Pos.Y, Z: placement.Pos.Z, RotY: placement.Rot.Y}
instID := placement.InstID
playerMotion := monsPos.ToMotionInfo()
spawnSource := "monster-placement"
hasAnchor := false
if anchors := res.SceneAnchors[entryID]; len(anchors) > 0 {
anchor := anchors[0] // PearlSR scene_mgr.zig getAnchorMotion.
playerMotion = (&common.Position{X: anchor.Pos.X, Y: anchor.Pos.Y, Z: anchor.Pos.Z, RotY: anchor.Rot.Y}).ToMotionInfo()
spawnSource = "anchor"
hasAnchor = true
}
SceneDebugLog("[CHALLENGE] spawn entry=%d floor=%d source=%s player=%v monster=%v", entryID, floorID, spawnSource, playerMotion, monsPos)
s.resetTechniqueRoom()
// NOTE: keep player.Scene intact so returning to overworld or relogging spawns in overworld
sceneInfo := &goproto.SceneInfo{
FloorId: floorID,
PlaneId: planeID,
EntryId: entryID,
GameModeType: sceneModeChallenge,
LeaderEntityId: 1,
WorldId: scene.WorldID,
SceneIdentifier: &goproto.SceneIdentifier{FloorId: floorID},
SceneMissionInfo: &goproto.MissionStatusBySceneInfo{},
FloorSavedData: scene.SavedValues,
}
sceneInfo.LightenSectionList = scene.Sections
// load all props in the arena scene (do not filter out prop groups like group 2 / group 3)
propEntityID := uint32(1000)
s.PropEntities = map[uint32]PropEntry{}
s.CurrentSceneFloor = floorID
for _, gid := range sortedU32Keys2(scene.Scenes) {
group := scene.Scenes[gid]
groupInfo := &goproto.SceneEntityGroupInfo{State: 1, GroupId: gid, PropertyMap: s.sceneGroupProperties(floorID, gid, scene.GroupProperties[gid])}
for i := range group.Props {
prop := &group.Props[i]
propEntityID++
s.PropEntities[propEntityID] = PropEntry{GroupID: prop.GroupID, PropID: prop.PropID, InstID: prop.InstID, State: prop.PropState}
groupInfo.EntityList = append(groupInfo.EntityList, &goproto.SceneEntityInfo{
InstId: prop.InstID,
GroupId: prop.GroupID,
EntityId: propEntityID,
Motion: (&common.Position{X: prop.Pos.X, Y: prop.Pos.Y, Z: prop.Pos.Z, RotY: prop.Rot.Y}).ToMotionInfo(),
Entity: &goproto.SceneEntityInfo_Prop{
Prop: &goproto.ScenePropInfo{PropState: prop.PropState, PropId: prop.PropID},
},
})
}
if len(groupInfo.EntityList) > 0 {
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, groupInfo)
}
}
// single boss monster (entity 30001, registered for SceneCastSkill battles)
monsterGroup := &goproto.SceneEntityGroupInfo{State: 1, GroupId: targetGroupID}
s.MonsterEntities = map[uint32]uint32{30001: eventID}
s.SceneMonsterIdentities = map[uint32]sceneMonsterIdentity{30001: {MonsterID: monsterID, GroupID: targetGroupID}}
s.ChallengeStageID = eventID
monsterGroup.EntityList = append(monsterGroup.EntityList, &goproto.SceneEntityInfo{
InstId: instID,
GroupId: targetGroupID,
EntityId: 30001,
Motion: monsPos.ToMotionInfo(),
Entity: &goproto.SceneEntityInfo_NpcMonster{
NpcMonster: &goproto.SceneNpcMonsterInfo{
MonsterId: monsterID,
EventId: eventID,
WorldLevel: playerWorldLevel,
},
},
})
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, monsterGroup)
// player team actors
avatarsToUse := avatarIDs
if len(avatarsToUse) == 0 {
for _, slot := range player.SortedLineupKeys() {
if v := player.Lineups[slot]; v != 0 {
avatarsToUse = append(avatarsToUse, v)
}
}
}
playerGroup := &goproto.SceneEntityGroupInfo{State: 0, GroupId: 0}
for slot, aid := range avatarsToUse {
playerGroup.EntityList = append(playerGroup.EntityList, &goproto.SceneEntityInfo{
EntityId: uint32(slot) + 1,
Motion: playerMotion,
Entity: &goproto.SceneEntityInfo_Actor{
Actor: &goproto.SceneActorInfo{
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
BaseAvatarId: aid,
MapLayer: 0,
Uid: playerUID,
},
},
})
}
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, playerGroup)
// NOTE: do not overwrite the saved scene so the player returns to the
// open world on logout/battle end
return sceneInfo, playerMotion, hasAnchor, nil
}
// handleStartChallengePeak ports on_start_challenge_peak_cs_req (cmd 8948):
// StartChallengePeakScRsp has no scene field, so the arena is sent via
// EnterSceneByServerScNotify (cmd 1427).
func handleStartChallengePeak(s *Session, req *goproto.StartChallengePeakCsReq, rsp *goproto.StartChallengePeakScRsp) error {
rsp.Retcode = 0
peakID := req.PeakId
stage, ok := getChallengeData().Peak[itoa(int(peakID))]
if !ok {
Log("[PEAK] peak_id=" + itoa(int(peakID)) + " not found in challenge_data!")
rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)
return nil
}
if stage.SceneResourceError != "" {
SceneDebugLog("[PEAK] scene resource rejected peak=%d: %s", peakID, stage.SceneResourceError)
rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)
return nil
}
eventID := stage.EventID
monsterID := stage.MonsterID
Log("[PEAK] Start: peak_id=" + itoa(int(peakID)) + " event=" + itoa(int(eventID)) + " monster=" + itoa(int(monsterID)))
avatars := dedupeTruncate4(req.PeakAvatarIdList)
if len(avatars) == 0 {
avatars = challengeAvatarsFallback(s)
}
buffs := append([]uint32(nil), stage.Buffs...)
if stage.IsBoss && s.PeakHardModes[stage.GroupID] {
eventID = stage.HardEventID
buffs = append([]uint32(nil), stage.HardBuffs...)
}
if req.BossBuffId != 0 {
buffs = append(buffs, req.BossBuffId)
}
if stage.Entrance == 0 || stage.Group == 0 || monsterID == 0 || eventID == 0 || len(avatars) == 0 {
rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)
return nil
}
previousConfig := s.ChallengeBattleConfig
if err := setPeakBattleConfig(s, avatars, peakID, eventID, buffs); err != nil {
SceneDebugLog("[PEAK] stage rejected: %v", err)
rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)
return nil
}
sceneInfo, playerMotion, hasAnchor, err := loadChallengeScene(s, stage.Entrance, stage.Group, monsterID, eventID, avatars)
if err != nil {
s.ChallengeBattleConfig = previousConfig
SceneDebugLog("[PEAK] scene rejected: %v", err)
rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)
return nil
}
s.ChallengeRun = &challengeRun{ID: peakID, Peak: true, Single: true, Rooms: []challengeRoom{{Entrance: stage.Entrance, Group: stage.Group, Monster: monsterID, Event: eventID, Avatars: avatars, Buffs: buffs}}, Scores: make([]uint32, 1), PeakBuff: req.BossBuffId}
lineupInfo := lineupInfoFromAvatars(avatars, sceneInfo.PlaneId, goproto.ExtraLineupType_ExtraLineupType_LineupChallenge, 0)
// Match the Client-tested PearlSR AA transition: send the arena scene,
// move each actor to the scene anchor when present, then let the typed
// dispatcher send StartChallengePeakScRsp last. The lineup is included in
// EnterSceneByServerScNotify, so a separate SyncLineupNotify is unnecessary.
if err := s.Send(&goproto.EnterSceneByServerScNotify{
Scene: sceneInfo,
Lineup: lineupInfo,
}); err != nil {
return err
}
anchorMoves := 0
if hasAnchor {
for _, group := range sceneInfo.EntityGroupList {
for _, entity := range group.EntityList {
if entity.GetActor() == nil {
continue
}
if err := s.Send(&goproto.SceneEntityMoveScNotify{
EntityId: entity.EntityId,
EntryId: sceneInfo.EntryId,
Motion: playerMotion,
}); err != nil {
return err
}
anchorMoves++
}
}
}
SceneDebugLog("[PEAK] transition peak=%d entry=%d anchor_moves=%d", peakID, sceneInfo.EntryId, anchorMoves)
return nil
}
var _ = proto.Marshal