package gameserver import ( "fmt" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) func (s *Session) techniqueLineup() *goproto.LineupInfo { var lineup *goproto.LineupInfo if run := s.ChallengeRun; run != nil { lineup = run.activeLineup(0) } else if p := s.JSON(); p != nil { lineup = presetLineup(p, p.ActiveTeamPreset) } else { return nil } rules, err := common.LoadTechniqueRules() if err != nil { return lineup } if s.techniqueMP == nil { v := rules.InitialMP if s.ChallengeRun != nil { v = lineup.MaxMp } s.techniqueMP = &v SceneDebugLog("[TECHNIQUE] initialized challenge=%t mp=%d/%d", s.ChallengeRun != nil, v, lineup.MaxMp) } if *s.techniqueMP > lineup.MaxMp { *s.techniqueMP = lineup.MaxMp } lineup.Mp = *s.techniqueMP if int(s.leaderSlot) < len(lineup.AvatarList) { lineup.LeaderSlot = s.leaderSlot } return lineup } // PearlSR services/battle.zig uses this protocol entity namespace for techniques. const techniqueAvatarEntityOffset uint32 = 100000 func (s *Session) techniqueAvatar(entity uint32) uint32 { lineup := s.techniqueLineup() if lineup == nil { return 0 } if entity == 0 { entity = lineup.LeaderSlot + 1 } for _, a := range lineup.AvatarList { if a.Slot+1 == entity || (entity >= techniqueAvatarEntityOffset && a.Id == entity-techniqueAvatarEntityOffset) { return a.Id } } return 0 } func (s *Session) resetTechniqueRoom() { s.techniqueMP = nil s.leaderSlot = 0 s.paidTechniqueCasts = nil } func (s *Session) syncTechnique(reason goproto.SyncLineupReason) error { return s.Send(&goproto.SyncLineupNotify{Lineup: s.techniqueLineup(), ReasonList: []goproto.SyncLineupReason{reason}}) } func techniqueMaterials(p *common.FreesrData) ([]*goproto.Material, error) { if err := p.EnsureTechniqueFoods(); err != nil { return nil, err } var out []*goproto.Material for _, id := range sortedU32Keys2(p.TechniqueFoodStock) { out = append(out, &goproto.Material{Tid: id, Num: p.TechniqueFoodStock[id]}) } return out, nil } func (s *Session) consumeTechniqueFood(id, count uint32) error { p := s.JSON() if p == nil { return fmt.Errorf("no player") } if _, active := CURRENT_BATTLE.Load(s.Token); active { return fmt.Errorf("cannot eat during battle") } rules, err := common.LoadTechniqueRules() if err != nil { return err } food, ok := rules.Foods[id] if !ok || count == 0 || count > food.MaxUse { return fmt.Errorf("unsupported food or count") } if err := p.EnsureTechniqueFoods(); err != nil { return err } if p.TechniqueFoodStock[id] < count { return fmt.Errorf("food %d is exhausted", id) } lineup := s.techniqueLineup() if lineup == nil || lineup.Mp >= lineup.MaxMp { return fmt.Errorf("technique points are full") } oldStock, oldMP := p.TechniqueFoodStock[id], *s.techniqueMP p.TechniqueFoodStock[id] -= count gain := uint64(food.RecoverMP)*uint64(count) + uint64(oldMP) if gain > uint64(lineup.MaxMp) { gain = uint64(lineup.MaxMp) } *s.techniqueMP = uint32(gain) if err := p.SavePersistentChecked(); err != nil { p.TechniqueFoodStock[id] = oldStock *s.techniqueMP = oldMP return err } if err := s.Send(&goproto.PlayerSyncScNotify{MaterialList: []*goproto.Material{{Tid: id, Num: p.TechniqueFoodStock[id]}}}); err != nil { return err } SceneDebugLog("[TECHNIQUE] food=%d count=%d mp=%d/%d remaining=%d", id, count, *s.techniqueMP, lineup.MaxMp, p.TechniqueFoodStock[id]) return s.syncTechnique(goproto.SyncLineupReason_SyncLineupReason_SyncReasonMpAdd) } func (s *Session) payTechnique(castID, skillIndex uint32, actor ...uint32) (uint32, error) { if s.paidTechniqueCasts[castID] { return 0, nil } rules, err := common.LoadTechniqueRules() if err != nil { return 0, err } entity := uint32(0) if len(actor) > 0 { entity = actor[0] } avatar := s.techniqueAvatar(entity) skills := rules.Avatars[avatar] if int(skillIndex) >= len(skills) { return uint32(goproto.Retcode_RET_MAZE_NO_ABILITY), nil } cost := skills[skillIndex].MPCost lineup := s.techniqueLineup() if lineup == nil { return uint32(goproto.Retcode_RET_LINEUP_IS_EMPTY), nil } p := s.JSON() if err := p.EnsureTechniqueFoods(); err != nil { return 0, err } if cost > lineup.MaxMp { return uint32(goproto.Retcode_RET_MAZE_MP_NOT_ENOUGH), nil } for *s.techniqueMP < cost && p.AutoTechniqueFood != nil && *p.AutoTechniqueFood { var selected uint32 for _, id := range sortedU32Keys2(rules.Foods) { if p.TechniqueFoodStock[id] > 0 { selected = id break } } if selected == 0 { break } if err := s.consumeTechniqueFood(selected, 1); err != nil { return 0, err } } if *s.techniqueMP < cost { return uint32(goproto.Retcode_RET_MAZE_MP_NOT_ENOUGH), nil } *s.techniqueMP -= cost if s.paidTechniqueCasts == nil { s.paidTechniqueCasts = map[uint32]bool{} } s.paidTechniqueCasts[castID] = true SceneDebugLog("[TECHNIQUE] cast=%d avatar=%d skill=%d cost=%d mp=%d/%d", castID, avatar, skillIndex, cost, *s.techniqueMP, lineup.MaxMp) return 0, nil } func init() { handle("UseItem", func(s *Session, req *goproto.UseItemCsReq, rsp *goproto.UseItemScRsp) error { rsp.UseItemId = req.UseItemId rsp.UseItemCount = req.UseItemCount if err := s.consumeTechniqueFood(req.UseItemId, req.UseItemCount); err != nil { SceneDebugLog("[TECHNIQUE] use item rejected: %v", err) rsp.Retcode = uint32(goproto.Retcode_RET_ITEM_USE_TARGET_TYPE_INVALID) } return nil }) }