400 lines
11 KiB
Go
400 lines
11 KiB
Go
package common
|
|
|
|
import (
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
// This file mirrors the proto conversion impls from the Rust crate
|
|
// (common/src/structs/{avatar,relic,lightcone,monster,scene}.rs).
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Lightcone
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (l *Lightcone) ToProtoEquipment() *goproto.Equipment {
|
|
return &goproto.Equipment{
|
|
Level: l.Level,
|
|
Promotion: l.Promotion,
|
|
Rank: l.Rank,
|
|
Tid: l.ItemID,
|
|
UniqueId: l.UniqueID(),
|
|
EquipAvatarId: l.EquipAvatar,
|
|
}
|
|
}
|
|
|
|
func (l *Lightcone) ToBattleEquipment() *goproto.BattleEquipment {
|
|
return &goproto.BattleEquipment{
|
|
Id: l.ItemID,
|
|
Level: l.Level,
|
|
Promotion: l.Promotion,
|
|
Rank: l.Rank,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Relic
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func relicAffixes(subs []SubAffix) []*goproto.RelicAffix {
|
|
out := make([]*goproto.RelicAffix, 0, len(subs))
|
|
for _, s := range subs {
|
|
out = append(out, &goproto.RelicAffix{AffixId: s.SubAffixID, Cnt: s.Count, Step: s.Step})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Relic) ToBattleRelic() *goproto.BattleRelic {
|
|
// Match PearlSR's standalone battle relic format. Inventory UniqueID is
|
|
// only sent by ToProtoRelic/ToEquipRelic; battle carries the full affixes.
|
|
return &goproto.BattleRelic{
|
|
Id: r.RelicID,
|
|
Level: r.Level,
|
|
MainAffixId: r.MainAffixID,
|
|
SubAffixList: relicAffixes(r.SubAffixes),
|
|
}
|
|
}
|
|
|
|
func (r *Relic) ToEquipRelic() *goproto.EquipRelic {
|
|
return &goproto.EquipRelic{
|
|
RelicSlot: r.Slot(),
|
|
RelicUniqueId: r.UniqueID(),
|
|
}
|
|
}
|
|
|
|
func (r *Relic) ToProtoRelic() *goproto.Relic {
|
|
return &goproto.Relic{
|
|
Level: r.Level,
|
|
MainAffixId: r.MainAffixID,
|
|
Tid: r.RelicID,
|
|
UniqueId: r.UniqueID(),
|
|
EquipAvatarId: r.EquipAvatar,
|
|
SubAffixList: relicAffixes(r.SubAffixes),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Avatar
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ToAvatarProto serializes the "base avatar" entry (8001 for MC, 1001 for March).
|
|
func (a *AvatarJson) ToAvatarProto(lightcone *Lightcone, mcID, marchID uint32) *goproto.Avatar {
|
|
baseAvatarID := BaseAvatarID(a.AvatarID)
|
|
|
|
var curMultiPath uint32
|
|
switch {
|
|
case IsMainCharacter(a.AvatarID):
|
|
curMultiPath = MultiPathAvatarFromID(mcID).ID()
|
|
case IsMarch7th(a.AvatarID):
|
|
curMultiPath = MultiPathAvatarFromID(marchID).ID()
|
|
default:
|
|
curMultiPath = baseAvatarID
|
|
}
|
|
|
|
var equipUID uint32
|
|
if lightcone != nil {
|
|
equipUID = lightcone.UniqueID()
|
|
}
|
|
|
|
return &goproto.Avatar{
|
|
BaseAvatarId: baseAvatarID,
|
|
Level: a.Level,
|
|
Promotion: a.Promotion,
|
|
EquipmentUniqueId: equipUID,
|
|
FirstMetTimestamp: 1712924677,
|
|
CurMultiPathAvatarType: curMultiPath,
|
|
HasTakenPromotionRewardList: []uint32{1, 3, 5},
|
|
}
|
|
}
|
|
|
|
// EnhanceAvatarID contains avatars that get enhanced mode enabled by default (March 7th Hunt, SP forms, etc.)
|
|
var EnhanceAvatarID = map[uint32]bool{
|
|
1004: true, 1005: true, 1006: true, 1102: true, 1205: true,
|
|
1212: true, 1217: true, 1306: true, 1307: true, 1310: true,
|
|
}
|
|
|
|
// AvatarExRankMap maps AvatarID to its corresponding Ex-Eidolon (Affinitive Eidolon) ID
|
|
var AvatarExRankMap = map[uint32]uint32{
|
|
1204: 120401, // Jing Yuan
|
|
1213: 121301, // Dan Heng • IL
|
|
1308: 130801, // Acheron
|
|
}
|
|
|
|
// ExRankAvatarID matches pearl-sr 4.6.53 (Jing Yuan 120401, Dan Heng IL 121301, Acheron 130801)
|
|
var ExRankAvatarID = []uint32{120401, 121301, 130801}
|
|
|
|
// DefaultAvatarSkinMap defines default skins if not explicitly set
|
|
var DefaultAvatarSkinMap = map[uint32]uint32{
|
|
1303: 1130301,
|
|
1001: 1100101,
|
|
1310: 1131001,
|
|
1415: 1141501,
|
|
1407: 1140701,
|
|
1501: 1150101,
|
|
1505: 1150501,
|
|
1409: 1140901,
|
|
}
|
|
|
|
func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic, dressedSkinID uint32) *goproto.AvatarPathData {
|
|
enhancedID := derefU32(a.EnhancedID)
|
|
if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] {
|
|
enhancedID = 1
|
|
}
|
|
if dressedSkinID == 0 {
|
|
dressedSkinID = DefaultAvatarSkinMap[a.AvatarID]
|
|
}
|
|
|
|
var exRanks []uint32
|
|
if a.ExRankUnlocked {
|
|
if exID, ok := AvatarExRankMap[a.AvatarID]; ok {
|
|
exRanks = []uint32{exID}
|
|
}
|
|
}
|
|
|
|
out := &goproto.AvatarPathData{
|
|
AvatarId: a.AvatarID,
|
|
Rank: a.Data.Rank,
|
|
EnhancedId: enhancedID,
|
|
DressedSkinId: dressedSkinID,
|
|
IGDEHGIKFJA: exRanks,
|
|
}
|
|
|
|
for _, r := range relics {
|
|
out.EquipRelicList = append(out.EquipRelicList, r.ToEquipRelic())
|
|
}
|
|
|
|
for anchorType, level := range a.Data.SkillsByAnchorType {
|
|
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
|
|
PointId: anchorType,
|
|
Level: level,
|
|
})
|
|
}
|
|
// deterministic order (Rust iterated the HashMap; order is irrelevant to the
|
|
// client, but stable output keeps packets reproducible)
|
|
sortSkillTree(out.AvatarPathSkillTree)
|
|
|
|
var pathEquip uint32
|
|
if lightcone != nil {
|
|
pathEquip = lightcone.UniqueID()
|
|
}
|
|
out.PathEquipmentId = pathEquip
|
|
|
|
return out
|
|
}
|
|
|
|
func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, relics []*Relic) (*goproto.BattleAvatar, []*goproto.BattleBuff) {
|
|
enhancedID := derefU32(a.EnhancedID)
|
|
if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] {
|
|
enhancedID = 1
|
|
}
|
|
|
|
var exRanks []uint32
|
|
if a.ExRankUnlocked {
|
|
if exID, ok := AvatarExRankMap[a.AvatarID]; ok {
|
|
exRanks = []uint32{exID}
|
|
}
|
|
}
|
|
|
|
ba := &goproto.BattleAvatar{
|
|
Index: index,
|
|
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
|
Id: a.AvatarID,
|
|
Level: a.Level,
|
|
Rank: a.Data.Rank,
|
|
Hp: 10000,
|
|
Promotion: a.Promotion,
|
|
WorldLevel: 6,
|
|
EnhancedId: enhancedID,
|
|
IGDEHGIKFJA: exRanks,
|
|
SpBar: &goproto.SpBarInfo{
|
|
CurSp: a.BattleSPCur(),
|
|
MaxSp: battleRatioScale,
|
|
},
|
|
}
|
|
|
|
for pointID, level := range a.Data.Skills {
|
|
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level})
|
|
}
|
|
sortAvatarSkillTree(ba.SkilltreeList)
|
|
|
|
if lightcone != nil {
|
|
ba.EquipmentList = append(ba.EquipmentList, lightcone.ToBattleEquipment())
|
|
}
|
|
|
|
for _, r := range relics {
|
|
ba.RelicList = append(ba.RelicList, r.ToBattleRelic())
|
|
}
|
|
|
|
var buffs []*goproto.BattleBuff
|
|
for _, buffID := range a.Techniques {
|
|
buffs = append(buffs, &goproto.BattleBuff{
|
|
Id: buffID,
|
|
Level: 1,
|
|
OwnerId: index,
|
|
WaveFlag: 0xffffffff,
|
|
TargetIndexList: []uint32{0},
|
|
DynamicValues: map[string]float32{"SkillIndex": 0},
|
|
})
|
|
}
|
|
|
|
return ba, buffs
|
|
}
|
|
|
|
func (a *AvatarJson) ToLineupAvatarProto(slot uint32) *goproto.LineupAvatar {
|
|
return &goproto.LineupAvatar{
|
|
Id: a.AvatarID,
|
|
Hp: 10000,
|
|
Satiety: 100,
|
|
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
|
SpBar: &goproto.SpBarInfo{
|
|
CurSp: a.SPCur(),
|
|
MaxSp: a.SPMaxVal(),
|
|
},
|
|
Slot: slot,
|
|
}
|
|
}
|
|
|
|
// ToLineupAvatars builds the overworld lineup (slots < 4, skipping empty/unknown).
|
|
func ToLineupAvatars(f *FreesrData) []*goproto.LineupAvatar {
|
|
known := func(id uint32) bool { _, ok := f.Avatars[id]; return ok }
|
|
|
|
var out []*goproto.LineupAvatar
|
|
for _, slot := range f.SortedLineupKeys() {
|
|
avatarID := f.Lineups[slot]
|
|
if slot >= 4 || avatarID == 0 || !known(avatarID) {
|
|
continue
|
|
}
|
|
out = append(out, f.Avatars[avatarID].ToLineupAvatarProto(slot))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ToLineupInfo builds LineupInfo from a lineup map (order: ascending slot).
|
|
func ToLineupInfo(lineups map[uint32]uint32) *goproto.LineupInfo {
|
|
maxMp := uint32(5)
|
|
if _, ok := lineups[1408]; ok {
|
|
maxMp = 8
|
|
}
|
|
|
|
lineup := &goproto.LineupInfo{
|
|
ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone,
|
|
Name: "Squad 1",
|
|
Mp: maxMp,
|
|
MaxMp: maxMp,
|
|
}
|
|
|
|
keys := make([]uint32, 0, len(lineups))
|
|
for k := range lineups {
|
|
keys = append(keys, k)
|
|
}
|
|
sortU32(keys)
|
|
|
|
for _, slot := range keys {
|
|
id := lineups[slot]
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
lineup.AvatarList = append(lineup.AvatarList, &goproto.LineupAvatar{
|
|
Id: id,
|
|
Hp: 10000,
|
|
Satiety: 100,
|
|
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
|
SpBar: &goproto.SpBarInfo{CurSp: 5000, MaxSp: 10000},
|
|
Slot: uint32(len(lineup.AvatarList)),
|
|
})
|
|
}
|
|
|
|
return lineup
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Monster
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
|
|
return &goproto.SceneMonster{
|
|
MonsterId: m.MonsterID,
|
|
// Zero requests the stage's normal HP, as in PearlSR addMonsterWaves.
|
|
MaxHp: m.MaxHP,
|
|
}
|
|
}
|
|
|
|
func monsterWave(monsters []Monster) *goproto.SceneMonsterWave {
|
|
maxLevel := uint32(0)
|
|
for _, monster := range monsters {
|
|
if monster.Level > maxLevel {
|
|
maxLevel = monster.Level
|
|
}
|
|
}
|
|
if maxLevel == 0 {
|
|
maxLevel = 95
|
|
}
|
|
|
|
wave := &goproto.SceneMonsterWave{
|
|
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
|
|
// Include a level so the client can validate this wave directly instead
|
|
// of looking up monster data from the battle stage's static table. This
|
|
// matters when a Calyx request reuses a custom battle stage ID.
|
|
WaveParam: &goproto.SceneMonsterWaveParam{Level: maxLevel},
|
|
}
|
|
for i := range monsters {
|
|
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
|
|
}
|
|
return wave
|
|
}
|
|
|
|
func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave {
|
|
out := make([]*goproto.SceneMonsterWave, 0, len(waves))
|
|
for _, wave := range waves {
|
|
out = append(out, monsterWave(wave))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Position / Motion
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (p Position) ToMotionInfo() *goproto.MotionInfo {
|
|
return &goproto.MotionInfo{
|
|
Pos: &goproto.Vector{X: p.X, Y: p.Y, Z: p.Z},
|
|
Rot: &goproto.Vector{Y: p.RotY},
|
|
}
|
|
}
|
|
|
|
func VectorToProto(v Vector) *goproto.Vector { return &goproto.Vector{X: v.X, Y: v.Y, Z: v.Z} }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func derefU32(v *uint32) uint32 {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func sortU32(s []uint32) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && s[j] < s[j-1]; j-- {
|
|
s[j], s[j-1] = s[j-1], s[j]
|
|
}
|
|
}
|
|
}
|
|
|
|
func sortAvatarSkillTree(s []*goproto.AvatarSkillTree) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && s[j].PointId < s[j-1].PointId; j-- {
|
|
s[j], s[j-1] = s[j-1], s[j]
|
|
}
|
|
}
|
|
}
|
|
|
|
func sortSkillTree(s []*goproto.AvatarPathSkillTree) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && s[j].PointId < s[j-1].PointId; j-- {
|
|
s[j], s[j-1] = s[j-1], s[j]
|
|
}
|
|
}
|
|
}
|