*O,fF>by@.7((((((((((((((((( (!("(#($(%(&('((()(*(+(,(-(.(/(0(1(2+:,=,>,?,@,A,B,C,D,E,F,G,H,I,J,K,L,M,N,ONoneKCarRangeMaxMinBulldogVectorColor RelativeTimeEngineXY RangeVectorZ VehiclesCoreSystemHeadlightCoronaOffsetOutValInValKInertiaTensor UpdateDust RelativeSizeReceiveLocalizedMessage DrawScale bHardAttachKUpdateConstraintParams LaunchRocketTick StaticMesh BulldogDustStartLocationRangeUserTextureUseRotationFromSpinParticlesSpinsPerSecondRangeStartSpinRange UseSizeScaleUseRegularSizeScaleRespawnDeadParticles MaxParticlesClientPlaySoundAutomaticInitialSpawning DrawStyleSecondsBeforeInactiveLifetimeRangeKLinearDampingKAngularDampingbHighDetailOnly bClientOnlybKDoubleTickRate SizeScaleKParamsCollisionHeightCollisionRadiusProcessCarInput ColorScale StartFlip PackStateStartSizeRange AccelerationClientKDriverLeaveWheelFrontAlongWheelFrontAcrossWheelRearAlongKVehicleUpdateParamsPostNetBeginPlayUseColorScaleWheelRearAcross WheelVertMaxSteerAngleMaxBrakeTorque TorqueSplit SteerPropGap TorqueCurve SteerTorque DestroyedStopThreshold ChassisMassTireHandbrakeFrictionTireHandbrakeSlip SteerSpeedSuspStiffness TireSoftness TireSlipRate SuspDampingSuspHighLimit InterpCurvePointsTireLateralSlip TireRollSlipTireLateralFrictionTireRollFriction SuspLowLimit TireMinSlip Gameplay TireMassHandbrakeThreshTrigger FlipTorque FlipTimeMaxNetUpdateIntervalVehicleStateReceivedUsedBy TakeDamage KUpdateStateKImpact KApplyForce Lighting LightColor LightHueLightSaturation Movement DrawTypeRearTireClassbLightChangedbStaticFrontTireClassGearbOnlyAffectPawnsFlipCarMessage OutputBrake KCarState KVehicleTooManyCarsMessage CollisionbCollideWhenPlacing SoundRadiusGetOutMessageFrontTriggerOffsetHeadlightOffset GetInMessageBTReTriggerDelayBulldogTrigger BulldogTireBulldogRocketBulldogMessageKarmaReverseMaterialForceBulldogHeadlightCoronaBulldogHeadlightBulldogFactoryBrakeMaterialTargetMaterial XWeapons KCOMOffsetTailOffMaterialHeadlightOnMaterialHeadlightOffMaterialKStartEnabledUntilNextImpact DustDropUseParticleColorStartVelocityRange DustSlipRateDustSlipThreshTriggerSpeedThreshDestroyedEffect UniformSizeDestroyedSound FireIntervalTargettingIntervalRocketFireOffsetStyleTargettingRangeEnginePitchScale KFrictionHitSoundThreshold AlphaTestTimerNoneSquealVelThresh bIsUnique bFadeMessagebBeep Lifetime DrawColor StackModePosY GetStringFrameBufferBlendingOp ProjTextureFOVMaxTraceDistance bClipBSPbProjectOnUnlit bGradientbProjectOnAlphabProjectOnParallelBSPAttachProjectorDetachProjector bSpecialHUD AirSpeedHealthDrawHUDFadeOutStartTime MaxSpritePPSTouchDampingFactorRange UseCollisionMaxVehicleCountNone MaxMeshPPSFadeOutExitPositions DrivePos PreBeginPlayClientPlayForceFeedback PawnDied Emitters KDriverLeave VehicleFire bNoDelete TryToDriveClientKDriverEnter KDriverEnter bWasAltFire GraphData bForceLeavebVehicleIsAltFiringOldPos TriggerTime bIsPlayerbestAim BestDistbVehicleIsFiringbEnableWeaponForceFeedbackParentFactory bAutoDriveDriverXPos WheelJoint RollFriction RollSlip LateralSlipMinSlip SlipRate Adhesion RestitutionbTireOnGroundGroundSlipVel SpinSpeed ReceiveStatebReceiveStateNewKConstraintActor1KConstraintActor2KPos1 KPriAxis1 KSecAxis1KPos2 KPriAxis2 KSecAxis2 Throttle KSteerAngleKProportionalGapKMaxSteerTorqueKMaxSteerSpeedbKSteeringLocked KMotorTorque KBrakingStructPropertyKSuspHighLimitKSuspStiffness KSuspDamping SteeringRotZ VehicleCountCreatedVehicleArrayPropertyClassPropertyRotYRotX NewTargetCameraRotationCameraLocation IsDemoBuildNetMode TimeSecondsPC instigatedByPYPosOptionalObject RelatedPRI_2 RelatedPRI_1 Momentum HitLocationDamageParticlesPerSecondInitialParticlesPerSecondEventInstigatorTorque impactNorm impactVel Softness ParticlesAllParticlesDeadLateralFriction ProjectedWorldToScreenSizeXSizeYGetCameraLocationSetPosOther KIsAwakeKWakeKSetImpactThresholdpos KSetMassbDestroyOnSimErrorObjectPropertyKRBVecFromVectorScriptedSequenceShaders XEffectsRocketExplosionEpicParticlesFlares FlashFlare1 WeaponSoundsP1RocketLauncherAltFirePRocketLauncherAltFire SeekLostLockOnRocketLauncherFireRocketLauncherSpriteEmitter1Simple MeshEmitter1XGamexPawn InitialDirKRBVecToVectorDamTypeSuperShockBeamSeekingRocketProjInterfaceContent fbBombFocus ForceDirSeekingVelMag FireLocationKGetRigidBodyState ScreenPosRatioXRatioYtileXtileYFloatProperty BoolProperty IntPropertyKGetRBQuaternionSetRelativeRotationDeltaSetDrawScale3DAngVelLinVel Quaternion PositionKRigidBodyStatebCarFlipTriggerKRBVec SoundPitchSkins Instigator VehicleFX DustyCloud2BullHeadlights BDHeadlightsBDHeadlights_ONBDTaiLight_ONBDReverselight_ON BDTailOff frontLeft frontRight rearLeft rearRightbHiddenOwnerKVehicleFactory LocalMessageEffectsDynamicProjector NewStateQuatFromAxisAndAngleQuatFindBetweenQuatRotateVector QuatInvert QuatProductInterpCurveEvalitAcosAsinSuspRef ReturnValue Velocity Rotation Location StrPropertyRG TireAdhesionTireRestitutionStructRotator FunctionObjectQuat TextBufferCBA ScriptText KConstraintWheelSpinSpeed ForwardVel bIsInverted IsDriving FlipTimeLeftNextNetUpdateTimeKCarWheelJoint OutputTorqueOutputHandbrakeOnKarmaParamsRBFullChassisPositionChassisQuaternionChassisLinVelChassisAngVel WheelHeightFrontWheelAng WheelVertVelServerSteering ServerTorque ServerBrakeServerHandbrakeOn bNewState ChassisState CarState bNewCarState ChassisYSteerY ChassisZcalcPosWheelYlPos chassisPos WheelLinVelwPosRelrelQWheelQ KarmaParamswPos ChassisX oldLinVel WheelState torqueScale worldForwardworldUp worldRight torqueAxisKTirePusher AmbientSoundtanasFactorBulldogMeshes S_Chassis S_RearWheel LevelInfo FLTrigger FRTrigger FlipTriggerHUD Headlight HeadlightOnPlayerReplicationInfo ControllerPlayerController DamageTypeCanvasHeadlightCorona BytePropertyLevelDustShader FinalBlend MeshEmitterEmitter TriggerStateFlipTriggerStatePawnSpriteEmitterFireCountdownSinceLastTargettingCurrentTarget MaterialParticleEmitterActor SoundGroupProceduralSoundBulldogStartSoundBulldogIdleSoundSoundBulldogSquealSoundRoleBulldogHitSoundBulldogStartForceBulldogIdleForceBulldogSquealForceBulldogHitForce OldDriverClass PosDotDirCamDir TotalSlip EnginePitchVMag EffectMeshesDirtChunk_01awPackageSwitchKSuspLowLimit SpritePPSMeshPPS KParams0NoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNone GetBestEntry MoveTarget AIControllerSVehicleFactoryVehicle RouteGoalKVehicleClassNone HealthMaxCurrentChassisStateNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneN n N WN N N YN N N N N N N WQF (z7z@u@u̱̱̮,e̮,e̒3}3}̱` E$=uSxS7$B6$CN N N N @N N CN N N cX@ O K܃WWWW=W=W=WW=W=W=W=W=W=W= F"#L"#SYfgVSN N 6N N XN N N N ,N N N N S N N 'N N DN N N N N N VN N FN N AN N aN N N C N N *N N bN N N N 1N N N N _N ~N N N N %N N 2N N N N {N N 7N N N N -N N N ]JN`.gq vwx"y"z{|}~t$ff&?N N N N N N N N N N C N N vN N mN .N HN N N N /N N 8N N )N N N N >N \M=G~v op!$>eN N :N N kN qdkO9:9:$GB Y6V@uqIučh(zh(z,e,e,e,e u u3}.g.g=G~=G~ O K O K O K O KWWWW3}(z(z(z(z(z(zҽhҽhҽҽҽҽҽҽҽҽҽo , u uhh3}(z(z(z3}.g O K=G~3}3}(z(z(z(z(z(z(z(z3}.g.g O K O K O K O K=G~WWWW3}  B:%C AC:HHBKPTUV:GLBdBGLdBBB=BA7A>LB|BB9B AX$CY$A\$333@]$=^$ B_ab$fff?c$?d:4Cf$@EQg$`Ii$Am$pAvwsw?$@$BA$BE$BF$pG$TEH$\BI$?J$CL$`jFR$@FS$CV$hAW$?^$]$?\$?[$u=Z$ #=U$B`;T$Q$L>P$O$AN$ BKZX1YY+$$CN $PCH$CN $|H$N N zO:%NY1HCBHBCBBAB$@E{$HDC" r$>@$C5eN N BN N N N `N N XN N N N NN N xN N ~N N HN N KN N LN N N N xN =N ;N N mN N QN TN cN 9N N IN rN [OI2V%F q~mnJLD]Press [Use] To Enter Vehicle.A]Press [Use] To Exit Vehicle.}]Too Many Cars Already!y]Press [Use] To Flip Vehicle.nopq"r*st$=N N N N PN N XN YN ZN !N N [N ]N ^N N N "N #N N N N N JN N gN aN iN lN HN 0N N jN { N N N Z^H+o ,P.tʁ)R N N N N N N N N uN N N N ]N N N N BN N 3N N AN N N N 5N N IN N _N N }N N |N N N N NN N zN N N N FN N \N N N N N N IN N lN N UN N N N DJCBAJKLaY (JKL#???aY (JKL#??a^ 'J2KL#???a^ 'J2KL#??6(66a  9?,2 #? #? # #? #?o$ 6a  9?,2 #? #? # #? #?o$ 6'62a  9?,2 #? #? # #? #?o$ 62a  9?,2 #? #? # #? #?o$ B GN dN oN N N WN N FN LM Naaaaaaaa G N N N RN N SN N fN `N N N N SN N ^N N N TN N [N N \N N (N `N N pN N gN cN N N N {e!neS9?,H9?%d9:9:$8;? 9?&g? 9?&g? g? g- ffffUV@9?Vr?U9?,('GV@U?rGV@U?rN-1'f"Uf"U""9?%e GN UN N YN hN hN qN N iN N DN N RN N wN N kN N MN N zN Vdl9:9:$9:9:$GB Y7JqIuĐ>WWWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWWWWWWWWWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWwaWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWwaWwaWwaWwaWWWWWWWWWWwaWwaWwaWwaWwaWwaWwaWwaWwaWWwaWwaWwaWwaWwaWwaWwaWwaWwaWWwaWwaWwaWwaWwaWwaWwaWwaWwaWWwaWwaWwaWwaWwaWwaWwaWwaWwa3}3}WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW  ?$4@$ CA$ CE$ CF$G$sEH$HBI$?J$zDL$zDR$`jFS$HBV$@W$?^$]$?\$?[${=Z$u=_$o:U$o:T$Q9a$?b$zDQ$u=P$O$@N$BKZX1YY+$$CN $oH$CN $H$N N d$Ce$@@f$>w"N N oN N MN N ]N N N N N YG@w冬ŃW $>7$B6$B5hN N vN sN N yN N pN N N |N N GN |N N ZN N VN N DN PN N N GN HN zN sN N N@N N LN MN xN O@N N P@N N @N N NN N EN N RN N EN N nN N fN E{N UN RN N ZN mN N rN N [N tN N yN N N N hN N ^@N N N gd>d GN ebT"<W7-bW-(*'9( GN iuYl+uu-' V& GN WN N naXa GN f_9:9:$ t]roaPG9?%-_Ma o o9P9?tL>rM3a $ (d.wd*d-dQRocketLauncherFireGCB GN N N kN N lN N o\k w\* v9?% GN QpM=9:9:$>a:a)a9:9:$5a%,ai%,MaQ9:9:$a~ a G N Lq+C` L+*q9:9:$>a  6<+6<6<*>a*>a'((:a  6<+6<6<*:a*:a'((-I')a  )-q')a*) aCHC)a(((-H(9:9:$5a . 6{+6{6{*5a*5"0%a  (+կX*%a*&a  (+կX*&a*,a  '+2կX*,a*,a  '+2կX*,a*&p,s7%7,7Ma = a7x7Ma*7K9:9:$.-(.-(.-(.-(.-( GN <PVK a GN jGU:f!3-G0G9?%G-G GN @N rDN # r3* ]B\B 3 TRS99R[3 TS[9?% |9? DI9?Cs]|u\|D9D6F9D6$69=,69=,69=,69=,09?Ds?9?Fu?&aGsuCC GN s@N u@N v@N wN KN yN }b8dLba#ca#?9b-86cL>{-89@Rr*-'&- (>?%- '3 #<9R &%- '-/(0&- (-/' #}9R -/(- (-/(%- '-/'9R%- '-/(-1(%- (-/(09X -1' #< - '-1'8-1(   GN tN N @ \cat@9?cSSpinSpeedSTorquett GN N N A~:ka9?% [ GN BN N B@l)_9?% `a#ya#?Aa#?Zy#?Vy#?I@Bի^VZ G N QBPz-'-'-(agh-(aghljhiljhiljhiljhiY Y Y Y Yca"_^][Zca"_^][Zca"_^][Zca"_^][Z G N dteu'deb9:9:$1w*-m'9-m(xe3w* xDXSREaPJ\#bJUXEFw\3r\*. * . * 3\xGeR-(bb-'9:9:$ - (&{% - '&p&z5@J-m5,y!%G!,!M-(!,s!%!,!M-'!_%PQ&PQ,PQ,PQvekBSH9?,,@9Ck~CnnML*9:9:$D)-8 DT -I(>a'((:a'((-I'ww-8 DT -I'>a(((:a(((-I(-8 DT -H()a'((-H'-8 DT -H')a(((-H( GN F&;=-Kl6lF6lN6lH6EI6C +H9?,IF9?&6} =6~=CCZ- 6E6l6 6l6C6l6`6l@6 #? 6 #? %6&6 %%66&66#?%6b@6;6 FN;%6#666 6 666 6 666 6  %6&6 %&66&66#?&6b@6&6#666 6 666 6 666 6  %6&6 %,66&;6 FN;,6#666 6 666 6 666 6 66#? %6&6 %,66&;6 FN;,6#666 6 666 6 666 6 66#?6~6} -6_- -6`-1-6p' GN N N GTj),+79:9:$ -n(T-n(' G N W?g~-6p Fr* r* r* r* 66E66 66C66`6EA6CB6`?6 #?06 #?66E66 66C66`46(66%66 64#?k0%6b?-k64-,AB,%6#064-'46(66&66 64#?k0&6b?-k64-,AB,&6#064-'46'66,66 64#?-?64-,AB,,6#064-'46'66,66 64#?-?64-,AB,,6#064-'6~ 6}- -6_-1-6`-6p(-n' G N cN N N I  GN @N N KN N LN N MN N JQuCQ %J@ !&N@ /,O@ =,P@  G N Cli `7 l% (rE ,d?9D?9?l%w*->.Raa GN UKh'^A%-q.@ :K>.@ WK GN SN N TN N EN N jOG W.O3rW*rW* rWrWw.W*hW j9?%ij i-qW  V%W  V, GN J// Base class for 4-wheeled vehicles using Karma // Assumes negative-X is forward, negative-Y is right class KCar extends KVehicle abstract; var KTire frontLeft, frontRight, rearLeft, rearRight; var (KCar) class FrontTireClass; var (KCar) class RearTireClass; // Wheel positions var const float WheelFrontAlong; var const float WheelFrontAcross; var const float WheelRearAlong; var const float WheelRearAcross; var const float WheelVert; var (KCar) float MaxSteerAngle; // (65535 = 360 deg) var (KCar) float MaxBrakeTorque; // Braking torque applied to all four wheels. Positive only. var (KCar) float TorqueSplit; // front/rear drive torque split. 1 is fully RWD, 0 is fully FWD. 0.5 is standard 4WD. // KCarWheelJoint setting for steering (see KCarWheelJoint). Duplicated here for handiness. var (KCar) float SteerPropGap; var (KCar) float SteerTorque; var (KCar) float SteerSpeed; // KCarWheelSuspension setting var (KCar) float SuspStiffness; var (KCar) float SuspDamping; var (KCar) float SuspHighLimit; var (KCar) float SuspLowLimit; var (KCar) float SuspRef; // KTire settings. Duplicated here for handy tuning. var (KCar) float TireRollFriction; var (KCar) float TireLateralFriction; var (KCar) float TireRollSlip; var (KCar) float TireLateralSlip; var (KCar) float TireMinSlip; var (KCar) float TireSlipRate; var (KCar) float TireSoftness; var (KCar) float TireAdhesion; var (KCar) float TireRestitution; var (KCar) float TireMass; var (KCar) float HandbrakeThresh; // speed above which handbrake comes on =] var (KCar) float TireHandbrakeSlip; // Additional lateral slip when handbrake engaged var (KCar) float TireHandbrakeFriction; // Additional lateral friction when handbrake engaged var (KCar) float ChassisMass; var (KCar) float StopThreshold; // Forward velocity under which brakes become drive. var (KCar) InterpCurve TorqueCurve; // Engine RPM in, Torque out. var (KCar) float FlipTorque; var (KCar) float FlipTime; var (KCar) float MaxNetUpdateInterval; var int Gear; // 1 is forward, -1 is backward. Currently symmetric power/torque curve // Car output var float WheelSpinSpeed; // Current (averaged) RPM of rear wheels var float ForwardVel; // Component of cars velocity in its forward direction. var bool bIsInverted; // Updated in Tick - indicates if car is not upright. // Internal var bool IsDriving; var float FlipTimeLeft; var float NextNetUpdateTime; // Next time we should force an update of vehicles state. // Low-level drive data (this is replicated) var bool OutputBrake; var float OutputTorque; var bool OutputHandbrakeOn; // Networking struct KCarState { var KRBVec ChassisPosition; var Quat ChassisQuaternion; var KRBVec ChassisLinVel; var KRBVec ChassisAngVel; var float WheelHeight[4]; // FL, FR, RL, RR var float FrontWheelAng[2]; // FL, FR var float WheelVertVel[4]; //var float WheelSpinVel[4]; var float ServerSteering; var float ServerTorque; var bool ServerBrake; var bool ServerHandbrakeOn; var bool bNewState; // Set to true whenever a new state is received and should be processed }; var KRigidBodyState ChassisState; var KCarState CarState; // This is replicated to the car, and processed to update all the parts. var bool bNewCarState; // Indicated there is new data processed, and chassis RBState should be updated. replication { // We replicate the Gear for brake-lights etc. unreliable if(Role == ROLE_Authority) CarState, Gear; reliable if(Role == ROLE_Authority) FlipTimeLeft; } // When new information is received, see if its new. If so, pass bits off the the wheels. // Each part will then update its rigid body position via the KUpdateState event. // JTODO: This is where clever unpacking would happen. simulated event VehicleStateReceived() { local vector ChassisY, SteerY, ChassisZ, calcPos, WheelY, lPos; local vector chassisPos, chassisLinVel, chassisAngVel, WheelLinVel, wPosRel; local Quat relQ, WheelQ; if(!CarState.bNewState) return; // Don't do anything if car isn't started up. if(frontLeft == None || frontRight == None || rearLeft == None || rearRight == None) return; // Get root chassis info ChassisState.Position = CarState.ChassisPosition; ChassisState.Quaternion = CarState.ChassisQuaternion; ChassisState.LinVel = CarState.ChassisLinVel; ChassisState.AngVel = CarState.ChassisAngVel; chassisPos = KRBVecToVector(CarState.ChassisPosition); chassisLinVel = KRBVecToVector(CarState.ChassisLinVel); chassisAngVel = KRBVecToVector(CarState.ChassisAngVel); // Calc chassis state axes ChassisY = QuatRotateVector(CarState.ChassisQuaternion, vect(0, 1, 0)); ChassisZ = QuatRotateVector(CarState.ChassisQuaternion, vect(0, 0, 1)); // Get root chassis info ChassisState.Position = CarState.ChassisPosition; ChassisState.Quaternion = CarState.ChassisQuaternion; ChassisState.LinVel = CarState.ChassisLinVel; ChassisState.AngVel = CarState.ChassisAngVel; // Figure out new state of wheels // Wheel positions are only supplied with a chassis-space Z (vertical) value - X and Y are assumed no to change // Rear wheel orientations are not supplied. The only constraint is their Y-axis (axle) is parallel to // the chassis Y-axis. A quaternion is calculated to go from current orientation to fulfil that criteria, which // should produce minimum difference to the 'roll' of the wheel - which is allowed to differ on server and client. // For front wheel we do send the current 'steering' angle. That is added after the above process as a quaternion // around chassis Z (up). // For linear velocity of wheels - calculate based on linear and angular velocity of chassis, and add on vertical // component sent over the net. ////////////////////////// FRONT LEFT ////////////////////////// frontLeft.KGetRigidBodyState(frontLeft.ReceiveState); // Position lPos.X = WheelFrontAlong; lPos.Y = WheelFrontAcross; lPos.Z = CarState.WheelHeight[0]; calcPos = chassisPos + QuatRotateVector(CarState.ChassisQuaternion, lPos); // Convert from chassis state to world space frontLeft.ReceiveState.Position = KRBVecFromVector(calcPos); // Rotation wheelQ = frontLeft.KGetRBQuaternion(); WheelY = QuatRotateVector(wheelQ, vect(0, 1, 0)); SteerY = QuatRotateVector( QuatFromAxisAndAngle(ChassisZ, CarState.FrontWheelAng[0]), ChassisY ); relQ = QuatFindBetween(WheelY, SteerY); frontLeft.ReceiveState.Quaternion = QuatProduct(relQ, wheelQ); // Velocity wPosRel = calcPos - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); WheelLinVel += CarState.WheelVertVel[0] * ChassisZ; frontLeft.ReceiveState.LinVel = KRBVecFromVector(WheelLinVel); //frontLeft.ReceiveState.AngVel = KRBVecFromVector(chassisAngVel + (WheelY * CarState.WheelSpinVel[0])); frontLeft.bReceiveStateNew = true; ////////////////////////// FRONT RIGHT ////////////////////////// frontRight.KGetRigidBodyState(frontRight.ReceiveState); // Position lPos.X = WheelFrontAlong; lPos.Y = -WheelFrontAcross; lPos.Z = CarState.WheelHeight[1]; calcPos = chassisPos + QuatRotateVector(CarState.ChassisQuaternion, lPos); frontRight.ReceiveState.Position = KRBVecFromVector(calcPos); // Rotation wheelQ = frontRight.KGetRBQuaternion(); WheelY = QuatRotateVector(wheelQ, vect(0, 1, 0)); SteerY = QuatRotateVector( QuatFromAxisAndAngle(ChassisZ, CarState.FrontWheelAng[1]), ChassisY ); relQ = QuatFindBetween(WheelY, SteerY); frontRight.ReceiveState.Quaternion = QuatProduct(relQ, wheelQ); // Velocity wPosRel = calcPos - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); WheelLinVel += CarState.WheelVertVel[1] * ChassisZ; frontRight.ReceiveState.LinVel = KRBVecFromVector(WheelLinVel); //frontRight.ReceiveState.AngVel = KRBVecFromVector(chassisAngVel + (WheelY * CarState.WheelSpinVel[1])); frontRight.bReceiveStateNew = true; ////////////////////////// REAR LEFT ////////////////////////// rearLeft.KGetRigidBodyState(rearLeft.ReceiveState); // Position lPos.X = WheelRearAlong; lPos.Y = WheelFrontAcross; lPos.Z = CarState.WheelHeight[2]; calcPos = chassisPos + QuatRotateVector(CarState.ChassisQuaternion, lPos); rearLeft.ReceiveState.Position = KRBVecFromVector(calcPos); // Rotation wheelQ = rearLeft.KGetRBQuaternion(); WheelY = QuatRotateVector(wheelQ, vect(0, 1, 0)); relQ = QuatFindBetween(WheelY, ChassisY); rearLeft.ReceiveState.Quaternion = QuatProduct(relQ, wheelQ); // Velocity wPosRel = calcPos - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); WheelLinVel += CarState.WheelVertVel[2] * ChassisZ; rearLeft.ReceiveState.LinVel = KRBVecFromVector(WheelLinVel); //rearLeft.ReceiveState.AngVel = KRBVecFromVector(chassisAngVel + (WheelY * CarState.WheelSpinVel[2])); rearLeft.bReceiveStateNew = true; ////////////////////////// REAR RIGHT ////////////////////////// rearRight.KGetRigidBodyState(rearRight.ReceiveState); // Position lPos.X = WheelRearAlong; lPos.Y = -WheelFrontAcross; lPos.Z = CarState.WheelHeight[3]; calcPos = chassisPos + QuatRotateVector(CarState.ChassisQuaternion, lPos); rearRight.ReceiveState.Position = KRBVecFromVector(calcPos); // Rotation wheelQ = rearRight.KGetRBQuaternion(); WheelY = QuatRotateVector(wheelQ, vect(0, 1, 0)); relQ = QuatFindBetween(WheelY, ChassisY); rearRight.ReceiveState.Quaternion = QuatProduct(relQ, wheelQ); // Velocity wPosRel = calcPos - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); WheelLinVel += CarState.WheelVertVel[3] * ChassisZ; rearRight.ReceiveState.LinVel = KRBVecFromVector(WheelLinVel); //rearRight.ReceiveState.AngVel = KRBVecFromVector(chassisAngVel + (WheelY * CarState.WheelSpinVel[3])); rearRight.bReceiveStateNew = true; ////// OTHER ////// // Update control inputs Steering = CarState.ServerSteering; OutputTorque = CarState.ServerTorque; OutputBrake = CarState.ServerBrake; OutputHandbrakeOn = CarState.ServerHandbrakeOn; // Update flags CarState.bNewState = false; bNewCarState = true; // For debugging... //KDrawRigidBodyState(ChassisState, false); //KDrawRigidBodyState(frontLeft.ReceiveState, false); //KDrawRigidBodyState(frontRight.ReceiveState, false); //KDrawRigidBodyState(rearLeft.ReceiveState, false); //KDrawRigidBodyState(rearRight.ReceiveState, false); } // This only update the chassis. The wheels update themselves. simulated event bool KUpdateState(out KRigidBodyState newState) { // This should never get called on the server - but just in case! if(Role == ROLE_Authority || !bNewCarState) return false; // Apply received data as new position of car chassis. newState = ChassisState; bNewCarState = false; return true; //return false; } // Pack current state of whole car into the state struct, to be sent to the client. // Should only get called on the server. function PackState() { local vector lPos, wPos, chassisPos, chassisLinVel, chassisAngVel, wPosRel, WheelLinVel; local vector ChassisX, ChassisZ, WheelY, oldPos, oldLinVel; local KRigidBodyState CurrentChassisState, WheelState; // Get chassis state. KGetRigidBodyState(CurrentChassisState); chassisPos = KRBVecToVector(CurrentChassisState.Position); chassisLinVel = KRBVecToVector(CurrentChassisState.LinVel); chassisAngVel = KRBVecToVector(CurrentChassisState.AngVel); // Last position we sent oldPos = KRBVectoVector(CarState.ChassisPosition); oldLinVel = KRBVectoVector(CarState.ChassisLinVel); // See if state has changed enough, or enough time has passed, that we // should send out another update by updating the state struct. if( !KIsAwake() ) { return; // Never send updates if physics is at rest } if( VSize(oldPos - chassisPos) > 5 || VSize(oldLinVel - chassisLinVel) > 1 || Abs(CarState.ServerTorque - OutputTorque) > 0.1 || Abs(CarState.ServerSteering - Steering) > 0.1 || Level.TimeSeconds > NextNetUpdateTime ) { NextNetUpdateTime = Level.TimeSeconds + MaxNetUpdateInterval; } else { return; //NextNetUpdateTime = Level.TimeSeconds + MaxNetUpdateInterval; } CarState.ChassisPosition = CurrentChassisState.Position; CarState.ChassisQuaternion = CurrentChassisState.Quaternion; CarState.ChassisLinVel = CurrentChassisState.LinVel; CarState.ChassisAngVel = CurrentChassisState.AngVel; ChassisX = QuatRotateVector(CarState.ChassisQuaternion, vect(1, 0, 0)); ChassisZ = QuatRotateVector(CarState.ChassisQuaternion, vect(0, 0, 1)); // Get each wheel state. ////////////////////////// FRONT LEFT ////////////////////////// frontLeft.KGetRigidBodyState(WheelState); wPos = KRBVecToVector(WheelState.Position); lPos = QuatRotateVector(QuatInvert(CarState.ChassisQuaternion), wPos - chassisPos); // Convert from world to chassis state space CarState.WheelHeight[0] = lPos.Z; // X should be WheelFrontAlong, Y should be WheelFrontAcross // For front wheels - we store their current angle around Z as well. WheelY = QuatRotateVector(WheelState.Quaternion, vect(0, 1, 0)); CarState.FrontWheelAng[0] = -ASin(ChassisX Dot WheelY); // Find component of relative wheel linear velocity along suspension travel (chassisZ). wPosRel = KRBVecToVector(WheelState.Position) - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); CarState.WheelVertVel[0] = ((WheelState.LinVel.X - WheelLinVel.X)* ChassisZ.X) + ((WheelState.LinVel.Y - WheelLinVel.Y)* ChassisZ.Y) + ((WheelState.LinVel.Z - WheelLinVel.Z)* ChassisZ.Z); //CarState.WheelSpinVel[0] = KRBVecToVector(WheelState.AngVel) Dot WheelY; ////////////////////////// FRONT RIGHT ////////////////////////// frontRight.KGetRigidBodyState(WheelState); wPos = KRBVecToVector(WheelState.Position); lPos = QuatRotateVector(QuatInvert(CarState.ChassisQuaternion), wPos - chassisPos); CarState.WheelHeight[1] = lPos.Z; WheelY = QuatRotateVector(WheelState.Quaternion, vect(0, 1, 0)); CarState.FrontWheelAng[1] = -ASin(ChassisX Dot WheelY); CarState.WheelVertVel[1] = ((WheelState.LinVel.X - WheelLinVel.X)* ChassisZ.X) + ((WheelState.LinVel.Y - WheelLinVel.Y)* ChassisZ.Y) + ((WheelState.LinVel.Z - WheelLinVel.Z)* ChassisZ.Z); //CarState.WheelSpinVel[1] = KRBVecToVector(WheelState.AngVel) Dot WheelY; ////////////////////////// REAR LEFT ////////////////////////// rearLeft.KGetRigidBodyState(WheelState); wPos = KRBVecToVector(WheelState.Position); lPos = QuatRotateVector(QuatInvert(CarState.ChassisQuaternion), wPos - chassisPos); CarState.WheelHeight[2] = lPos.Z; wPosRel = KRBVecToVector(WheelState.Position) - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); CarState.WheelVertVel[2] = ((WheelState.LinVel.X - WheelLinVel.X)* ChassisZ.X) + ((WheelState.LinVel.Y - WheelLinVel.Y)* ChassisZ.Y) + ((WheelState.LinVel.Z - WheelLinVel.Z)* ChassisZ.Z); WheelY = QuatRotateVector(WheelState.Quaternion, vect(0, 1, 0)); //CarState.WheelSpinVel[2] = KRBVecToVector(WheelState.AngVel) Dot WheelY; ////////////////////////// REAR RIGHT ////////////////////////// rearRight.KGetRigidBodyState(WheelState); wPos = KRBVecToVector(WheelState.Position); lPos = QuatRotateVector(QuatInvert(CarState.ChassisQuaternion), wPos - chassisPos); CarState.WheelHeight[3] = lPos.Z; wPosRel = KRBVecToVector(WheelState.Position) - chassisPos; WheelLinVel = chassisLinVel + (chassisAngVel Cross wPosRel); CarState.WheelVertVel[3] = ((WheelState.LinVel.X - WheelLinVel.X)* ChassisZ.X) + ((WheelState.LinVel.Y - WheelLinVel.Y)* ChassisZ.Y) + ((WheelState.LinVel.Z - WheelLinVel.Z)* ChassisZ.Z); WheelY = QuatRotateVector(WheelState.Quaternion, vect(0, 1, 0)); //CarState.WheelSpinVel[3] = KRBVecToVector(WheelState.AngVel) Dot WheelY; // OTHER CarState.ServerSteering = Steering; CarState.ServerTorque = OutputTorque; CarState.ServerBrake = OutputBrake; CarState.ServerHandbrakeOn = OutputHandbrakeOn; // This flag lets the client know this data is new. CarState.bNewState = true; } simulated function PostNetBeginPlay() { local vector RotX, RotY, RotZ, lPos; Super.PostNetBeginPlay(); // Set up suspension graphics GetAxes(Rotation,RotX,RotY,RotZ); // Spawn wheels, and flip graphics where necessary frontLeft = spawn(FrontTireClass, self,, Location + WheelFrontAlong*RotX + WheelFrontAcross*RotY + WheelVert*RotZ, Rotation); //frontLeft.SetDrawScale(1); frontLeft.SetDrawScale3D(vect(1, 1, 1)); frontRight = spawn(FrontTireClass, self,, Location + WheelFrontAlong*RotX - WheelFrontAcross*RotY + WheelVert*RotZ, Rotation); frontRight.SetDrawScale3D(vect(1, -1, 1)); rearLeft = spawn(RearTireClass, self,, Location + WheelRearAlong*RotX + WheelRearAcross*RotY + WheelVert*RotZ, Rotation); //rearLeft.SetDrawScale(1); rearLeft.SetDrawScale3D(vect(1, 1, 1)); rearRight = spawn(RearTireClass, self,, Location + WheelRearAlong*RotX - WheelRearAcross*RotY + WheelVert*RotZ, Rotation); rearRight.SetDrawScale3D(vect(1, -1, 1)); // Create joints lPos.X = WheelFrontAlong; lPos.Y = WheelFrontAcross; lPos.Z = WheelVert; frontLeft.WheelJoint = spawn(class'KCarWheelJoint', self); frontLeft.WheelJoint.KPos1 = lPos/50; frontLeft.WheelJoint.KPriAxis1 = vect(0, 0, 1); frontLeft.WheelJoint.KSecAxis1 = vect(0, 1, 0); frontLeft.WheelJoint.KConstraintActor1 = self; frontLeft.WheelJoint.KPos2 = vect(0, 0, 0); frontLeft.WheelJoint.KPriAxis2 = vect(0, 0, 1); frontLeft.WheelJoint.KSecAxis2 = vect(0, 1, 0); frontLeft.WheelJoint.KConstraintActor2 = frontLeft; frontLeft.WheelJoint.SetPhysics(PHYS_Karma); lPos.Y = -WheelFrontAcross; frontRight.WheelJoint = spawn(class'KCarWheelJoint', self); frontRight.WheelJoint.KPos1 = lPos/50; frontRight.WheelJoint.KPriAxis1 = vect(0, 0, 1); frontRight.WheelJoint.KSecAxis1 = vect(0, 1, 0); frontRight.WheelJoint.KConstraintActor1 = self; frontRight.WheelJoint.KPos2 = vect(0, 0, 0); frontRight.WheelJoint.KPriAxis2 = vect(0, 0, 1); frontRight.WheelJoint.KSecAxis2 = vect(0, 1, 0); frontRight.WheelJoint.KConstraintActor2 = frontRight; frontRight.WheelJoint.SetPhysics(PHYS_Karma); lPos.X = WheelRearAlong; lPos.Y = WheelRearAcross; rearLeft.WheelJoint = spawn(class'KCarWheelJoint', self); rearLeft.WheelJoint.KPos1 = lPos/50; rearLeft.WheelJoint.KPriAxis1 = vect(0, 0, 1); rearLeft.WheelJoint.KSecAxis1 = vect(0, 1, 0); rearLeft.WheelJoint.KConstraintActor1 = self; rearLeft.WheelJoint.KPos2 = vect(0, 0, 0); rearLeft.WheelJoint.KPriAxis2 = vect(0, 0, 1); rearLeft.WheelJoint.KSecAxis2 = vect(0, 1, 0); rearLeft.WheelJoint.KConstraintActor2 = rearLeft; rearLeft.WheelJoint.SetPhysics(PHYS_Karma); lPos.Y = -WheelRearAcross; rearRight.WheelJoint = spawn(class'KCarWheelJoint', self); rearRight.WheelJoint.KPos1 = lPos/50; rearRight.WheelJoint.KPriAxis1 = vect(0, 0, 1); rearRight.WheelJoint.KSecAxis1 = vect(0, 1, 0); rearRight.WheelJoint.KConstraintActor1 = self; rearRight.WheelJoint.KPos2 = vect(0, 0, 0); rearRight.WheelJoint.KPriAxis2 = vect(0, 0, 1); rearRight.WheelJoint.KSecAxis2 = vect(0, 1, 0); rearRight.WheelJoint.KConstraintActor2 = rearRight; rearRight.WheelJoint.SetPhysics(PHYS_Karma); // Initially make sure parameters are sync'ed with Karma KVehicleUpdateParams(); } // Clean up wheels etc. simulated event Destroyed() { // Destroy joints holding wheels to car frontLeft.WheelJoint.Destroy(); frontRight.WheelJoint.Destroy(); rearLeft.WheelJoint.Destroy(); rearRight.WheelJoint.Destroy(); // Destroy wheels themselves. frontLeft.Destroy(); frontRight.Destroy(); rearLeft.Destroy(); rearRight.Destroy(); Super.Destroyed(); } // Call this if you change any parameters (tire, suspension etc.) and they // will be passed down to each wheel/joint. simulated event KVehicleUpdateParams() { Super.KVehicleUpdateParams(); rearLeft.WheelJoint.bKSteeringLocked = true; rearRight.WheelJoint.bKSteeringLocked = true; frontLeft.WheelJoint.bKSteeringLocked = false; frontLeft.WheelJoint.KProportionalGap = SteerPropGap; frontLeft.WheelJoint.KMaxSteerTorque = SteerTorque; frontLeft.WheelJoint.KMaxSteerSpeed = SteerSpeed; frontRight.WheelJoint.bKSteeringLocked = false; frontRight.WheelJoint.KProportionalGap = SteerPropGap; frontRight.WheelJoint.KMaxSteerTorque = SteerTorque; frontRight.WheelJoint.KMaxSteerSpeed = SteerSpeed; frontLeft.WheelJoint.KSuspHighLimit = SuspHighLimit; frontLeft.WheelJoint.KSuspLowLimit = SuspLowLimit; frontLeft.WheelJoint.KSuspStiffness = SuspStiffness; frontLeft.WheelJoint.KSuspDamping = SuspDamping; frontRight.WheelJoint.KSuspHighLimit = SuspHighLimit; frontRight.WheelJoint.KSuspLowLimit = SuspLowLimit; frontRight.WheelJoint.KSuspStiffness = SuspStiffness; frontRight.WheelJoint.KSuspDamping = SuspDamping; rearLeft.WheelJoint.KSuspHighLimit = SuspHighLimit; rearLeft.WheelJoint.KSuspLowLimit = SuspLowLimit; rearLeft.WheelJoint.KSuspStiffness = SuspStiffness; rearLeft.WheelJoint.KSuspDamping = SuspDamping; rearRight.WheelJoint.KSuspHighLimit = SuspHighLimit; rearRight.WheelJoint.KSuspLowLimit = SuspLowLimit; rearRight.WheelJoint.KSuspStiffness = SuspStiffness; rearRight.WheelJoint.KSuspDamping = SuspDamping; // Sync params with Karma. frontLeft.WheelJoint.KUpdateConstraintParams(); frontRight.WheelJoint.KUpdateConstraintParams(); rearLeft.WheelJoint.KUpdateConstraintParams(); rearRight.WheelJoint.KUpdateConstraintParams(); // Mass KSetMass(ChassisMass); frontLeft.KSetMass(TireMass); frontRight.KSetMass(TireMass); rearLeft.KSetMass(TireMass); rearRight.KSetMass(TireMass); // Tire params handy tuning frontLeft.RollFriction = TireRollFriction; frontLeft.LateralFriction = TireLateralFriction; frontLeft.RollSlip = TireRollSlip; frontLeft.LateralSlip = TireLateralSlip; frontLeft.MinSlip = TireMinSlip; frontLeft.SlipRate = TireSlipRate; frontLeft.Softness = TireSoftness; frontLeft.Adhesion = TireAdhesion; frontLeft.Restitution = TireRestitution; frontRight.RollFriction = TireRollFriction; frontRight.LateralFriction = TireLateralFriction; frontRight.RollSlip = TireRollSlip; frontRight.LateralSlip = TireLateralSlip; frontRight.MinSlip = TireMinSlip; frontRight.SlipRate = TireSlipRate; frontRight.Softness = TireSoftness; frontRight.Adhesion = TireAdhesion; frontRight.Restitution = TireRestitution; rearLeft.RollFriction = TireRollFriction; rearLeft.LateralFriction = TireLateralFriction; rearLeft.RollSlip = TireRollSlip; rearLeft.LateralSlip = TireLateralSlip; rearLeft.MinSlip = TireMinSlip; rearLeft.SlipRate = TireSlipRate; rearLeft.Softness = TireSoftness; rearLeft.Adhesion = TireAdhesion; rearLeft.Restitution = TireRestitution; rearRight.RollFriction = TireRollFriction; rearRight.LateralFriction = TireLateralFriction; rearRight.RollSlip = TireRollSlip; rearRight.LateralSlip = TireLateralSlip; rearRight.MinSlip = TireMinSlip; rearRight.SlipRate = TireSlipRate; rearRight.Softness = TireSoftness; rearRight.Adhesion = TireAdhesion; rearRight.Restitution = TireRestitution; } // Possibly apply force to flip car over. simulated event KApplyForce(out vector Force, out vector Torque) { local float torqueScale; local vector worldForward, worldUp, worldRight, torqueAxis; if(FlipTimeLeft == 0) return; worldForward = vect(-1, 0, 0) >> Rotation; worldUp = vect(0, 0, 1) >> Rotation; worldRight = vect(0, 1, 0) >> Rotation; torqueAxis = Normal(worldUp Cross vect(0, 0, 1)); // Torque scaled by how far over we are. // This will be between 0 and PI - so convert to between 0 and 1. torqueScale = Acos(worldUp Dot vect(0, 0, 1))/3.1416; Torque = FlipTorque * torqueScale * torqueAxis; } function StartFlip(Pawn Pusher) { //local vector toPusher, worldUp; // if we are already flipping the car - dont do it again! if(FlipTimeLeft > 0) return; // Dont let you push the car if you are going to be underneath it! //worldUp = vect(0, 0, 1) >> Rotation; //toPusher = Pusher.Location - Location; //if( (worldUp Dot toPusher) < 0) // return; FlipTimeLeft = FlipTime; // Start the flip on the server } // Tell it your current throttle, and it will give you an output torque // This is currently like an electric motor function float Engine(float Throttle) { local float torque; torque = Abs(Throttle) * Gear * InterpCurveEval(TorqueCurve, WheelSpinSpeed); GraphData("SpinSpeed", WheelSpinSpeed); GraphData("Torque", torque); return -1 * torque; } function ProcessCarInput() { local vector worldForward, worldUp; //Log("PCI S:"$Steering$" T:"$Throttle); worldForward = vect(-1, 0, 0) >> Rotation; worldUp = vect(0, 0, 1) >> Rotation; ForwardVel = Velocity Dot worldForward; bIsInverted = worldUp.Z < 0.2; // 'ForwardVel' isn't very helpful if we are inverted, so we just pretend its positive. if(bIsInverted) ForwardVel = 2 * StopThreshold; //Log("F:"$ForwardVel$"IsI:"$bIsInverted); if( Driver == None ) { if(bAutoDrive == true) { Gear = 1; OutputBrake = false; Throttle = 0.4; Steering = 1; //log("Thr:"$Throttle); KWake(); } else { Gear = 0; OutputBrake = true; } } else { if(Throttle > 0.01) // pressing forwards { if(ForwardVel < -StopThreshold && Gear != 1) // going backwards - so brake first { //Log("F - Brake"); Gear = 0; OutputBrake = true; IsDriving = false; } else // stopped or going forwards, so drive { //Log("F - Drive"); Gear = 1; OutputBrake = false; IsDriving = true; } } else if(Throttle < -0.01) // pressing backwards { // We have to release the brakes and then press reverse again to go into reverse if(ForwardVel < StopThreshold && IsDriving == false) { //Log("B - Drive"); Gear = -1; OutputBrake = false; IsDriving = false; } else // otherwise, we are going forwards, or still holding brake, so just brake { //Log("B - Brake"); Gear = 0; OutputBrake = true; IsDriving = true; } } else // not pressing either { // If stationary, stick brakes on if(Abs(ForwardVel) < StopThreshold) { //Log("B - Brake"); Gear = 0; OutputBrake = true; IsDriving = false; OutputHandbrakeOn = false; // force handbrake off if stopped. } else // otherwise, coast { //Log("Coast"); Gear = 0; OutputBrake = false; IsDriving = false; } } KWake(); // currently, never let the car go to sleep whilst being driven. } // If we are going forwards, steering, and pressing the brake, // enable extra-slippy handbrake. if((ForwardVel > HandbrakeThresh || OutputHandbrakeOn == true) && Abs(Steering) > 0.01 && OutputBrake == true) OutputHandbrakeOn = true; else OutputHandbrakeOn = false; // Engine model OutputTorque = Engine(Throttle); } // Car Simulation simulated function Tick(float Delta) { local float tana, sFactor; Super.Tick(Delta); WheelSpinSpeed = (rearLeft.SpinSpeed + rearRight.SpinSpeed)/2; //log("WheelSpinSpeed:"$WheelSpinSpeed); // if we are in the process of flipping the car, keep it enabled! if( FlipTimeLeft > 0 ) KWake(); // If the server, process input and pack updated car info into struct. if(Role == ROLE_Authority) { ProcessCarInput(); PackState(); } // Motor // FRONT frontLeft.WheelJoint.KMotorTorque = 0.5 * OutputTorque * (1-TorqueSplit); frontRight.WheelJoint.KMotorTorque = 0.5 * OutputTorque * (1-TorqueSplit); // REAR rearLeft.WheelJoint.KMotorTorque = 0.5 * OutputTorque * TorqueSplit; rearRight.WheelJoint.KMotorTorque = 0.5 * OutputTorque * TorqueSplit; // Braking if(OutputBrake) { frontLeft.WheelJoint.KBraking = MaxBrakeTorque; frontRight.WheelJoint.KBraking = MaxBrakeTorque; rearLeft.WheelJoint.KBraking = MaxBrakeTorque; rearRight.WheelJoint.KBraking = MaxBrakeTorque; } else { frontLeft.WheelJoint.KBraking = 0.0; frontRight.WheelJoint.KBraking = 0.0; rearLeft.WheelJoint.KBraking = 0.0; rearRight.WheelJoint.KBraking = 0.0; } // Steering tana = Tan(6.283/65536 * Steering * MaxSteerAngle); sFactor = 0.5 * tana * (2 * WheelFrontAcross) / Abs(WheelFrontAlong - WheelRearAlong); frontLeft.WheelJoint.KSteerAngle = 65536/6.283 * Atan(tana, (1-sFactor)); frontRight.WheelJoint.KSteerAngle = 65536/6.283 * Atan(tana, (1+sFactor)); // Handbrake if(OutputHandbrakeOn == true) { //Log("HANDBRAKE!!"); rearLeft.LateralFriction = TireLateralFriction + TireHandbrakeFriction; rearLeft.LateralSlip = TireLateralSlip + TireHandbrakeSlip; rearRight.LateralFriction = TireLateralFriction + TireHandbrakeFriction; rearRight.LateralSlip = TireLateralSlip + TireHandbrakeSlip; } else { rearLeft.LateralFriction = TireLateralFriction; rearLeft.LateralSlip = TireLateralSlip; rearRight.LateralFriction = TireLateralFriction; rearRight.LateralSlip = TireLateralSlip; } // Flipping if(FlipTimeLeft > 0) { FlipTimeLeft -= Delta; FlipTimeLeft = FMax(FlipTimeLeft, 0.0); // Make sure it doesn't go negative } } N pclass BulldogTrigger extends ScriptedSequence notplaceable; var float BTReTriggerDelay; var float TriggerTime; var bool bCarFlipTrigger; function Touch( Actor Other ) { local xPawn User; User = xPawn(Other); if ( (User == None) || (User.Controller == None) ) return; if ( ((User.Controller.RouteGoal == self) || (User.Controller.Movetarget == self)) && (AIController(User.Controller) != None) ) { UsedBy(User); return; } if ( BTRetriggerDelay > 0 ) { if ( Level.TimeSeconds - TriggerTime < BTReTriggerDelay ) return; TriggerTime = Level.TimeSeconds; } // Send a string message to the toucher. if(!bCarFlipTrigger) User.ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 0); else User.ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 3); } function UsedBy( Pawn user ) { if(bCarFlipTrigger) { Bulldog(Owner).StartFlip(User); } else { // Enter vehicle code Bulldog(Owner).TryToDrive(User); } } N }O^O-OwBmOw9?}%B%B%-(zmOw9?|&z&z&-([%%&& GN %class BulldogTire extends KTire; N b class BulldogRocket extends SeekingRocketProj; // Seeks harder than normal homing rockets simulated function Timer() { local vector ForceDir; local float VelMag; if ( InitialDir == vect(0,0,0) ) InitialDir = Normal(Velocity); Acceleration = vect(0,0,0); Super.Timer(); if ( (Seeking != None) && (Seeking != Instigator) ) { // Do normal guidance to target. ForceDir = Normal(Seeking.Location - Location); if( (ForceDir Dot InitialDir) > -0.5 ) { VelMag = VSize(Velocity); ForceDir = Normal( (ForceDir * 0.5 * VelMag) + (0.8 * Velocity) ); Velocity = VelMag * ForceDir; Acceleration += 5 * ForceDir; } // Update rocket so it faces in the direction its going. SetRotation(rotator(Velocity)); } } N I class BulldogMessage extends LocalMessage; var localized string GetInMessage; var localized string GetOutMessage; var localized string TooManyCarsMessage; var localized string FlipCarMessage; static function string GetString( optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject ) { switch(Switch) { case 0: return Default.GetInMessage; break; case 1: return Default.GetOutMessage; break; case 2: return Default.TooManyCarsMessage; break; case 3: return Default.FlipCarMessage; break; } } N i#exec OBJ LOAD FILE=..\textures\EpicParticles.utx class BulldogHeadlightCorona extends Effects; N w#exec OBJ LOAD FILE=VehicleFX.utx class BulldogHeadlight extends DynamicProjector; // Empty tick here - do detach/attach in Bulldog tick function Tick(float Delta) { } N 4k $##w* w4 4v44?vL?v49?,4a+9P GN d class BulldogFactory extends KVehicleFactory; event Trigger( Actor Other, Pawn EventInstigator ) { local KVehicle CreatedVehicle; if(EventInstigator.IsA('Vehicle')) return; if(VehicleCount >= MaxVehicleCount) { if(EventInstigator != None) EventInstigator.ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 2); return; } if( KVehicleClass != None ) { CreatedVehicle = spawn(KVehicleClass, , , Location, Rotation); if ( CreatedVehicle != None ) { VehicleCount++; CreatedVehicle.ParentFactory = self; } else log("Couldn't spawn Bulldog"); } } _aO@`Gսi Yhh> y@N N bcAa/!w HFwA*A  V, w*Na wN*NCouldn't spawn Bulldog GN AN N d#exec OBJ LOAD FILE=..\staticmeshes\EffectMeshes.usx #exec OBJ LOAD FILE=..\textures\VehicleFX.utx class BulldogDust extends Emitter; var () int MaxSpritePPS; var () int MaxMeshPPS; simulated function UpdateDust(KTire t, float DustSlipRate, float DustSlipThresh) { local float SpritePPS, MeshPPS; //Log("Material:"$t.GroundMaterial$" OnGround:"$t.bTireOnGround); // If wheel is on ground, and slipping above threshold.. if(t.bTireOnGround && t.GroundSlipVel > DustSlipThresh) { SpritePPS = FMin(DustSlipRate * (t.GroundSlipVel - DustSlipThresh), MaxSpritePPS); Emitters[0].ParticlesPerSecond = SpritePPS; Emitters[0].InitialParticlesPerSecond = SpritePPS; Emitters[0].AllParticlesDead = false; MeshPPS = FMin(DustSlipRate * (t.GroundSlipVel - DustSlipThresh), MaxMeshPPS); Emitters[1].ParticlesPerSecond = MeshPPS; Emitters[1].InitialParticlesPerSecond = MeshPPS; Emitters[1].AllParticlesDead = false; } else // ..otherwise, switch off. { Emitters[0].ParticlesPerSecond = 0; Emitters[0].InitialParticlesPerSecond = 0; Emitters[1].ParticlesPerSecond = 0; Emitters[1].InitialParticlesPerSecond = 0; } } N n#exec OBJ LOAD FILE=..\staticmeshes\BulldogMeshes.usx #exec OBJ LOAD FILE=..\sounds\WeaponSounds.uax #exec OBJ LOAD FILE=..\textures\VehicleFX.utx class Bulldog extends KCar placeable CacheExempt; // triggers used to get into the Bulldog var const vector FrontTriggerOffset; var BulldogTrigger FLTrigger, FRTrigger, FlipTrigger; // headlight projector var const vector HeadlightOffset; var BulldogHeadlight Headlight; var bool HeadlightOn; // Light materials var Material ReverseMaterial; var Material BrakeMaterial; var Material TailOffMaterial; var Material HeadlightOnMaterial; var Material HeadlightOffMaterial; var BulldogHeadlightCorona HeadlightCorona[8]; var (Bulldog) vector HeadlightCoronaOffset[8]; // Used to prevent triggering sounds continuously var float UntilNextImpact; // Wheel dirt emitter var BulldogDust Dust[4]; // FL, FR, RL, RR // Distance below centre of tire to place dust. var float DustDrop; // Ratio between dust kicked up and amount wheels are slipping var (Bulldog) float DustSlipRate; var (Bulldog) float DustSlipThresh; // Maximum speed at which you can get in the vehicle. var (Bulldog) float TriggerSpeedThresh; var bool TriggerState; // true for on, false for off. var bool FlipTriggerState; // Destroyed Buggy var (Bulldog) class DestroyedEffect; var (Bulldog) sound DestroyedSound; // Weapon var float FireCountdown; var float SinceLastTargetting; var Pawn CurrentTarget; var (Bulldog) float FireInterval; var (Bulldog) float TargettingInterval; var (Bulldog) vector RocketFireOffset; // Position (car ref frame) that rockets are launched from var (Bulldog) float TargettingRange; var (Bulldog) Material TargetMaterial; // Bulldog sound things var (Bulldog) float EnginePitchScale; var (Bulldog) sound BulldogStartSound; var (Bulldog) sound BulldogIdleSound; var (Bulldog) float HitSoundThreshold; var (Bulldog) sound BulldogSquealSound; var (Bulldog) float SquealVelThresh; var (Bulldog) sound BulldogHitSound; var (Bulldog) String BulldogStartForce; var (Bulldog) String BulldogIdleForce; var (Bulldog) String BulldogSquealForce; var (Bulldog) String BulldogHitForce; replication { reliable if(Role == ROLE_Authority) HeadlightOn, CurrentTarget; } function PreBeginPlay() { Super.PreBeginPlay(); if ( Level.IsDemoBuild() ) Destroy(); } simulated function PostNetBeginPlay() { local vector RotX, RotY, RotZ; local int i; Super.PostNetBeginPlay(); GetAxes(Rotation,RotX,RotY,RotZ); // Only have triggers on server if(Level.NetMode != NM_Client) { // Create triggers for gettting into the Bulldog FLTrigger = spawn(class'BulldogTrigger', self,, Location + FrontTriggerOffset.X * RotX + FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); FLTrigger.SetBase(self); FLTrigger.SetCollision(true, false, false); FRTrigger = spawn(class'BulldogTrigger', self,, Location + FrontTriggerOffset.X * RotX - FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); FRTrigger.SetBase(self); FRTrigger.SetCollision(true, false, false); TriggerState = true; // Create trigger for flipping the bulldog back over. FlipTrigger = spawn(class'BulldogTrigger', self,, Location); Fliptrigger.bCarFlipTrigger = true; FlipTrigger.SetBase(self); FlipTrigger.SetCollisionSize(350, 200); FlipTrigger.SetCollision(false, false, false); FlipTriggerState = false; } // Dont bother making emitters etc. on dedicated server if(Level.NetMode != NM_DedicatedServer) { // Create headlight projector. We make sure it doesn't project on the vehicle itself. Headlight = spawn(class'BulldogHeadlight', self,, Location + HeadlightOffset.X * RotX + HeadlightOffset.Y * RotY + HeadlightOffset.Z * RotZ); Headlight.SetBase(self); Headlight.SetRelativeRotation(rot(-2000, 32768, 0)); // Create wheel dust emitters. Dust[0] = spawn(class'BulldogDust', self,, Location + WheelFrontAlong*RotX + WheelFrontAcross*RotY + (WheelVert-DustDrop)*RotZ); Dust[0].SetBase(self); Dust[1] = spawn(class'BulldogDust', self,, Location + WheelFrontAlong*RotX - WheelFrontAcross*RotY + (WheelVert-DustDrop)*RotZ); Dust[1].SetBase(self); Dust[2] = spawn(class'BulldogDust', self,, Location + WheelRearAlong*RotX + WheelRearAcross*RotY + (WheelVert-DustDrop)*RotZ); Dust[2].SetBase(self); Dust[3] = spawn(class'BulldogDust', self,, Location + WheelRearAlong*RotX - WheelRearAcross*RotY + (WheelVert-DustDrop)*RotZ); Dust[3].SetBase(self); // Set light materials Skins[1]=BrakeMaterial; Skins[2]=HeadlightOffMaterial; // Spawn headlight coronas for(i=0; i<8; i++) { HeadlightCorona[i] = spawn(class'BulldogHeadlightCorona', self,, Location + (HeadlightCoronaOffset[i] >> Rotation) ); HeadlightCorona[i].SetBase(self); } } // For KImpact event KSetImpactThreshold(HitSoundThreshold); // If this is not 'authority' version - don't destroy it if there is a problem. // The network should sort things out. if(Role != ROLE_Authority) { KarmaParams(KParams).bDestroyOnSimError = False; KarmaParams(frontLeft.KParams).bDestroyOnSimError = False; KarmaParams(frontRight.KParams).bDestroyOnSimError = False; KarmaParams(rearLeft.KParams).bDestroyOnSimError = False; KarmaParams(rearRight.KParams).bDestroyOnSimError = False; } } simulated event Destroyed() { local int i; //Log("Bulldog Destroyed"); // Clean up random stuff attached to the car if(Level.NetMode != NM_Client) { FLTrigger.Destroy(); FRTrigger.Destroy(); FlipTrigger.Destroy(); } if(Level.NetMode != NM_DedicatedServer) { Headlight.Destroy(); for(i=0; i<4; i++) Dust[i].Destroy(); for(i=0; i<8; i++) HeadlightCorona[i].Destroy(); } // This will destroy wheels & joints Super.Destroyed(); // Trigger destroyed sound and effect if(Level.NetMode != NM_DedicatedServer) { spawn(DestroyedEffect, self, , Location ); PlaySound(DestroyedSound); } } function KImpact(actor other, vector pos, vector impactVel, vector impactNorm) { /* Only make noises for Bulldog-world impacts */ if(other != None) return; if(UntilNextImpact < 0) { //PlaySound(BulldogHitSound); // hack to stop the sound repeating rapidly on impacts //UntilNextImpact = GetSoundDuration(BulldogHitSound); } } simulated function ClientKDriverEnter(PlayerController pc) { //Log("Bulldog ClientKDriverEnter"); Super.ClientKDriverEnter(pc); } function KDriverEnter(Pawn p) { //Log("Bulldog KDriverEnter"); Super.KDriverEnter(p); //PlaySound(BulldogStartSound); //AmbientSound=BulldogIdleSound; p.bHidden = True; ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 1); } simulated function ClientKDriverLeave(PlayerController pc) { //Log("Bulldog ClientKDriverLeave"); Super.ClientKDriverLeave(pc); } function bool KDriverLeave(bool bForceLeave) { local Pawn OldDriver; //Log("Bulldog KDriverLeave"); OldDriver = Driver; // If we succesfully got out of the car, make driver visible again. if( Super.KDriverLeave(bForceLeave) ) { OldDriver.bHidden = false; AmbientSound = None; return true; } else return false; } function LaunchRocket(bool bWasAltFire) { local vector RotX, RotY, RotZ, FireLocation; local BulldogRocket R; local PlayerController PC; // Client can't do firing if(Role != ROLE_Authority) return; // Fire as many rockets as we have time to. GetAxes(Rotation, RotX, RotY, RotZ); FireLocation = Location + (RocketFireOffset >> Rotation); while(FireCountdown <= 0) { if(!bWasAltFire) { // Fire a rocket at the current target (starts flying forward and up a bit) R = spawn(class'Vehicles.BulldogRocket', self, , FireLocation, rotator(-1 * RotX + 0.2 * RotZ)); R.Seeking = CurrentTarget; // Play firing noise PlaySound(Sound'WeaponSounds.RocketLauncher.RocketLauncherFire', SLOT_None,,,,, false); PC = PlayerController(Controller); if (PC != None && PC.bEnableWeaponForceFeedback) PC.ClientPlayForceFeedback("RocketLauncherFire"); } else { //Log("Would AltFire At:"$CurrentTarget); } FireCountdown += FireInterval; } } // Fire a rocket (if we've had time to reload!) function VehicleFire(bool bWasAltFire) { Super.VehicleFire(bWasAltFire); if(FireCountdown < 0) { FireCountdown = 0; LaunchRocket(bWasAltFire); } } // For drawing targetting reticule simulated function DrawHud(Canvas C) { local int XPos, YPos; local vector ScreenPos; local float RatioX, RatioY; local float tileX, tileY; local float SizeX, SizeY, PosDotDir; local vector CameraLocation, CamDir; local rotator CameraRotation; if(CurrentTarget == None) return; SizeX = 64.0; SizeY = 64.0; ScreenPos = C.WorldToScreen( CurrentTarget.Location ); // Dont draw reticule if target is behind camera C.GetCameraLocation( CameraLocation, CameraRotation ); CamDir = vector(CameraRotation); PosDotDir = (CurrentTarget.Location - CameraLocation) Dot CamDir; if( PosDotDir < 0) return; RatioX = C.SizeX / 640.0; RatioY = C.SizeY / 480.0; tileX = sizeX * RatioX; tileY = sizeY * RatioX; XPos = ScreenPos.X; YPos = ScreenPos.Y; C.Style = ERenderStyle.STY_Additive; C.DrawColor.R = 255; C.DrawColor.G = 255; C.DrawColor.B = 255; C.DrawColor.A = 255; C.SetPos(XPos - tileX*0.5, YPos - tileY*0.5); C.DrawTile( TargetMaterial, tileX, tileY, 0.0, 0.0, 256, 256); //--- TODO : Fix HARDCODED USIZE } simulated function Tick(float Delta) { Local float BestAim, BestDist, TotalSlip, EnginePitch, VMag; Local vector RotX, RotY, RotZ, FireLocation; local pawn NewTarget; local int i; Super.Tick(Delta); // Weapons (run on server and replicated to client) if(Role == ROLE_Authority) { // Only put headlights on when driver is in. Save batteries! if(Driver != None) HeadlightOn=true; else HeadlightOn=false; // Do targetting (do nothing if no controller) SinceLastTargetting += Delta; if(Controller != None && SinceLastTargetting > TargettingInterval) { GetAxes(Rotation, RotX, RotY, RotZ); FireLocation = Location + (RocketFireOffset >> Rotation); // BestAim is zero - so we will only target things in front of the buggy BestAim = 0; NewTarget = Controller.PickTarget(BestAim, BestDist, -1.0 * RotX, FireLocation, TargettingRange); // If this is a new lock - play lock-on noise if(NewTarget != CurrentTarget) { if(NewTarget == None) PlayerController(Controller).ClientPlaySound(Sound'WeaponSounds.SeekLost'); else PlayerController(Controller).ClientPlaySound(Sound'WeaponSounds.LockOn'); } CurrentTarget = NewTarget; SinceLastTargetting = 0; } // Countdown to next shot FireCountdown -= Delta; // This is for sustained barrages. // Primary fire takes priority if(bVehicleIsFiring) LaunchRocket(false); else if(bVehicleIsAltFiring) LaunchRocket(true); } // Dont bother doing effects on dedicated server. if(Level.NetMode != NM_DedicatedServer) { // Update brake light colors. if(Gear == -1 && OutputBrake == false) { // REVERSE Skins[1] = ReverseMaterial; } else if(Gear == 0 && OutputBrake == true) { // BRAKE Skins[1] = BrakeMaterial; } else { // OFF Skins[1] = TailOffMaterial; } // Set headlight projector state. Headlight.DetachProjector(); if(HeadlightOn) { Headlight.AttachProjector(); Skins[2] = HeadlightOnMaterial; for(i=0; i<8; i++) HeadlightCorona[i].bHidden = false; } else { Skins[2] = HeadlightOffMaterial; for(i=0; i<8; i++) HeadlightCorona[i].bHidden = true; } // Update dust kicked up by wheels. Dust[0].UpdateDust(frontLeft, DustSlipRate, DustSlipThresh); Dust[1].UpdateDust(frontRight, DustSlipRate, DustSlipThresh); Dust[2].UpdateDust(rearLeft, DustSlipRate, DustSlipThresh); Dust[3].UpdateDust(rearRight, DustSlipRate, DustSlipThresh); } UntilNextImpact -= Delta; // This assume the sound is an idle-ing sound, and increases with pitch // as wheels speed up. EnginePitch = 64 + ((WheelSpinSpeed/EnginePitchScale) * (255-64)); SoundPitch = FClamp(EnginePitch, 0, 254); //Log("Engine Pitch:"$SoundPitch); //SoundVolume = 255; // Currently, just use rear wheels for slipping noise TotalSlip = rearLeft.GroundSlipVel + rearRight.GroundSlipVel; //log("TotalSlip:"$TotalSlip); if(TotalSlip > SquealVelThresh) { rearLeft.AmbientSound = BulldogSquealSound; } else { rearLeft.AmbientSound = None; } // Dont have triggers on network clients. if(Level.NetMode != NM_Client) { // If vehicle is moving, disable collision for trigger. VMag = VSize(Velocity); if(!bIsInverted && VMag < TriggerSpeedThresh && TriggerState == false) { FLTrigger.SetCollision(true, false, false); FRTrigger.SetCollision(true, false, false); TriggerState = true; } else if((bIsInverted || VMag > TriggerSpeedThresh) && TriggerState == true) { FLTrigger.SetCollision(false, false, false); FRTrigger.SetCollision(false, false, false); TriggerState = false; } // If vehicle is inverted, and slow, turn on big 'flip vehicle' trigger. if(bIsInverted && VMag < TriggerSpeedThresh && FlipTriggerState == false) { FlipTrigger.SetCollision(true, false, false); FlipTriggerState = true; } else if((!bIsInverted || VMag > TriggerSpeedThresh) && FlipTriggerState == true) { FlipTrigger.SetCollision(false, false, false); FlipTriggerState = false; } } } // Really simple at the moment! function TakeDamage(int Damage, Pawn instigatedBy, Vector hitlocation, Vector momentum, class damageType) { // Avoid damage healing the car! if(Damage < 0) return; if(damageType == class'DamTypeSuperShockBeam') Health -= 100; // Instagib doesn't work on vehicles else Health -= 0.5 * Damage; // Weapons do less damage // The vehicle is dead! if(Health <= 0) { if ( Controller != None ) { if( Controller.bIsPlayer ) { ClientKDriverLeave(PlayerController(Controller)); // Just to reset HUD etc. Controller.PawnDied(self); // This should unpossess the controller and let the player respawn } else Controller.Destroy(); } Destroy(); // Destroy the vehicle itself (see Destroyed below) } //KAddImpulse(momentum, hitlocation); } // AI Related code function Actor GetBestEntry(Pawn P) { if ( FlipTriggerState ) return FlipTrigger; if ( VSize(P.Location - FLTrigger.Location) < VSize(P.Location - FRTrigger.Location) ) return FLTrigger; return FRTrigger; } $AA@BS:L?333/$0$W1S2S3h$?N D(S#&'S`+S=:pA9Y=$*ZnN $=*",BN $fff?*$1@N $?*2PN )"dZ 8 Z$$N Z$$AN Z$$N N "$Z 8 Z$$N $?$?N $?N Z$>$?N Z$>$?N N ,-$.Z$@@$@@N [Z 8 Z$B$BN Z$$AN Z$pB$BN N N $ff?ff?ff?/$0$1S2S3N N jN N N N Pis)4:c-H)Zi > i : >: GN N J JBOBtBYBgVyiB^BFViByBmB`BXB|FPBWXDBb^ FfBs^ Bw^ FOB_FtFhXOBEVky_BBJRB`yeBjFgBQNSB`B[BQB_XXXzBM^k^l^Ed{JLFNJodVNm^JBrB}^r_xXwByy5^~^p^oXC^hXE^nXF^ ^ ^ BO^m_u^kFe^jXdyvXayUywFxFyJBFzF{F|_{FrF}^c^W^b^I^HV_C^D^L^GVZygBvB~yQFN`CJ|JHXtB@^XBXXXYX>XTXMX_j^@Bl{`LOyX[B^A^By|BjXUB^XkXKJTJrJF=XBXPXCJ`^sJmBu_dyxyt_`Xc`JVWXXgXi_]_c_fXsBayeXDXuXUd]XhXGdeXj`n`oXmXpXqXlXkXsJRJH[ hGSJJE@ !DAJ}BB~XuJD v?CdS uG uE uFdRBiBnBPBVd^XbVXXJXGdl qV qUyn_q !jv !jr gsJl !'qJi !%p gt guJHJ!i  Ix~yQz`xpwINFr]OA^lnzyM|F@fU}eAtDCwS@d\rBwQF`@o@\O~]elP}oLH[hiHwZEqSN FbqA@?N@Z\KlQ{BJCYNXidA|Oi^El@qzTL@\\~lH|cJbX@YfBu@BDMxS@XKzZqojmYnimxGHkWmf@ov@lE@kTQcPsKC@cRnbIq@\A@]ON]ak@^yRGIVV l@Y{aJUXTfRtTCUQ__nm[ |pI] WXdcsHBIPS^Vl^z{HWW@]eyvHjDvnj|SJ\Xeg]uVD]R@paLoN@CMZ\skrzYIGY!dhMxfGhUWd[tZC^QHaaq @CcORr!MCrR@dbMHqSyDITYQiJwAFQUveNuCDoSnbsqt@uOJ^KlL|{ KcFPULcRq_ENF]al\{EJWY@PiR+yKK@V[QkZ{@XJGxX@P@U_@To@K L O F\@_j@az~JMYei@by@cGaU@ff@Qt@gC[QVD`@io@z}@mMVA[V}jVyyKGKVPfAvbFGU{cb|OJf XeersPCdSZaKpX@\OK^Xn@>-}@TPj@Y@zMz@X.I@twfkgzvJ@k;ZQ@MVUL@CU k@P7@@UJw@@ A@DNN@\@~j@}x@|F@xV@wf8CvMy jHPr:5ALvl}FBMCd@pP;u@MujMCgRPLb cp.#~Ma\o]^PVu[a@iW|hPSYc`s_CGnS_4WA_4|X _T_40so_4n cV_4U Q=_4uf._4C[okR^ _4p pO` ck_!rJ _4pZ@_4{J@OFE  I`K  Klk O+W +ZB +MR @sda |E