package dnfbridge import ( "context" "fmt" "math" dnfcharstat "longheng.io/server/internal/modules/dnf/charstat" dnfcompanion "longheng.io/server/internal/modules/dnf/companion" dnfrepo "longheng.io/server/internal/modules/dnf/repository" ) const ( // Balanced support soldiers are intentionally durable because they have no // player-style resurrection flow. 5000% means 50x the owner's effective HP. currentDungeonCompanionBalanceHPPercent int64 = 5000 currentDungeonCompanionBalanceMPPercent int64 = 200 ) type currentDungeonCompanionProjectionOptions struct { EffectiveLevel int Mode1 []byte AIProfile dnfcompanion.AIProfile } type currentDungeonCompanionBalanceSummary struct { OwnerLevel int HPMax int64 MPMax int64 Strength int64 Intelligence int64 Vitality int64 Spirit int64 PhysicalDefense int64 MagicalDefense int64 PhysicalAttack int64 MagicalAttack int64 IndependentAttack int64 MoveSpeed int64 AttackSpeed int64 CastSpeed int64 } func scaleCompanionBalanceValue(value, percent int64) int64 { if value <= 0 || percent <= 0 { return 0 } if value > math.MaxInt64/percent { return math.MaxInt64 } return value * percent / 100 } func clampCompanionBalanceU32(value int64) int64 { if value < 0 { return 0 } if value > math.MaxUint32 { return math.MaxUint32 } return value } func buildCurrentDungeonCompanionBalancedVector( ownerStats dnfcharstat.Vector, level int, ) (dnfcharstat.Vector, currentDungeonCompanionBalanceSummary) { target := dnfcharstat.Vector{ HPMax: clampCompanionBalanceU32(scaleCompanionBalanceValue(ownerStats.HPMax, currentDungeonCompanionBalanceHPPercent)), MPMax: clampCompanionBalanceU32(scaleCompanionBalanceValue(ownerStats.MPMax, currentDungeonCompanionBalanceMPPercent)), // The balance switch is a projection of the owner's effective combat // vector. Copy each dimension independently instead of using the larger // side of a pair and applying a compensating multiplier: a STR class // must not gain the owner's INT, and a support APC must not lose the // owner's lower physical/magical attack side. Strength: ownerStats.Strength, Intelligence: ownerStats.Intelligence, Vitality: ownerStats.Vitality, Spirit: ownerStats.Spirit, PhysicalAttack: ownerStats.PhysicalAttack, MagicalAttack: ownerStats.MagicalAttack, IndependentAttack: ownerStats.IndependentAttack, PhysicalDefense: ownerStats.PhysicalDefense, MagicalDefense: ownerStats.MagicalDefense, FireResistance: ownerStats.FireResistance, WaterResistance: ownerStats.WaterResistance, DarkResistance: ownerStats.DarkResistance, LightResistance: ownerStats.LightResistance, InventoryLimit: ownerStats.InventoryLimit, HPRegenSpeed: ownerStats.HPRegenSpeed, MPRegenSpeed: ownerStats.MPRegenSpeed, MoveSpeed: ownerStats.MoveSpeed, AttackSpeed: ownerStats.AttackSpeed, CastSpeed: ownerStats.CastSpeed, HitRecovery: ownerStats.HitRecovery, JumpPower: ownerStats.JumpPower, Weight: ownerStats.Weight, } summary := currentDungeonCompanionBalanceSummary{ OwnerLevel: level, HPMax: target.HPMax, MPMax: target.MPMax, Strength: target.Strength, Intelligence: target.Intelligence, Vitality: target.Vitality, Spirit: target.Spirit, PhysicalDefense: target.PhysicalDefense, MagicalDefense: target.MagicalDefense, PhysicalAttack: target.PhysicalAttack, MagicalAttack: target.MagicalAttack, IndependentAttack: target.IndependentAttack, MoveSpeed: target.MoveSpeed, AttackSpeed: target.AttackSpeed, CastSpeed: target.CastSpeed, } return target, summary } func (s *Service) buildCurrentDungeonCompanionBalanceMode1( ctx context.Context, session *gameSession, state dnfrepo.CompanionSourceState, ) (int, []byte, currentDungeonCompanionBalanceSummary, error) { if s == nil || state.Owner.CharacterID == "" || state.Source.CharacterID == "" { return 0, nil, currentDungeonCompanionBalanceSummary{}, fmt.Errorf("companion balance owner/source missing") } level := state.Owner.Level if level < 1 || level > 90 { return 0, nil, currentDungeonCompanionBalanceSummary{}, fmt.Errorf("companion balance owner level=%d", level) } ownerStats, ok := s.characterPVFStatsForUserInfo(ctx, session, state.Owner, true) if !ok { return 0, nil, currentDungeonCompanionBalanceSummary{}, fmt.Errorf("companion balance owner PVF stats unavailable") } // Use the owner's currently worn PVF stat budget instead of the source // character's equipment. This is the normalization boundary: appearance, // profession and skills remain source-owned while combat stats are owner-owned. ownerStats = s.addEquipmentPVFStatsForUserInfo(ctx, session, state.Owner, ownerStats) target, summary := buildCurrentDungeonCompanionBalancedVector(ownerStats, level) synthetic := dnfrepo.CloneCharacter(state.Source) synthetic.Level = level synthetic.Stats = characterPVFStatValues(target) synthetic.Stats["grow_type"] = state.Source.Stats["grow_type"] synthetic.Stats["stat_level"] = csharpSubtype1ProtocolStatLevel // Refresh exactly the frozen runtime equipment used for AIC creation and // mode0: source avatars plus the companion's temporary balanced gear. reader := csharpLegacyUserInfoReader{ ctx: ctx, characterID: state.Source.CharacterID, service: s, session: session, suppressActorSkillResources: true, equipmentSnapshot: &state.Equipment, } // This body is embedded in the APC projection alongside its 0/0 mode0; // it is not a player row addressed to the advertised game server. mode1 := reader.buildCurrentUserInfoMode1WithEquipmentForOwner( synthetic, true, currentDungeonCompanionProjectionObjectKey, true, 0, currentSceneObjectRoute, currentSceneObjectContext, ) if len(mode1) <= currentDungeonCompanionMode1ObjectKeyOffset+1 { return 0, nil, currentDungeonCompanionBalanceSummary{}, fmt.Errorf("companion balance mode1 too short: %d", len(mode1)) } return level, mode1, summary, nil }