552 lines
16 KiB
Ucode
552 lines
16 KiB
Ucode
//=============================================================================
|
|
// KFPawn_ZedPatriarch_Versus
|
|
//=============================================================================
|
|
// Human controllable Patriarch pawn class for versus mode
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
//=============================================================================
|
|
class KFPawn_ZedPatriarch_Versus extends KFPawn_ZedPatriarch;
|
|
|
|
/** Cached controller */
|
|
var KFPlayerController MyKFPC;
|
|
|
|
/** The local player controller viewing this pawn */
|
|
var KFPlayerController ViewerPlayer;
|
|
|
|
/** The threshold at which to display a low health warning */
|
|
var float LowHealthThreshold;
|
|
|
|
/** Whether we've already warned of low health this phase */
|
|
var bool bWarnedLowHealthThisPhase;
|
|
|
|
/** used to keep track of heal message */
|
|
var bool bIsQuickHealMessageShowing;
|
|
|
|
/** Percentage of max health to allow healing at */
|
|
var private float HealThreshold;
|
|
|
|
/** Health threshold to perform an autoheal at */
|
|
var private float AutoHealThreshold;
|
|
|
|
/** Whether the Patriarch autohealed this phase or not */
|
|
var private bool bAutoHealed;
|
|
|
|
/** How long to summon children for */
|
|
var private float SummonChildrenDuration;
|
|
|
|
/** Determines if a summon has been done this phase yet */
|
|
var bool bSummonedThisPhase;
|
|
|
|
/** Number of charges left on our cloak */
|
|
var private repnotify byte CloakCharges;
|
|
|
|
/** Localized strings */
|
|
var localized string LowHealthMsg;
|
|
var localized string NoHealsRemainingMsg;
|
|
var localized string NoMortarTargetsMsg;
|
|
|
|
replication
|
|
{
|
|
if( bNetOwner && bNetDirty )
|
|
MyKFPC;
|
|
|
|
if( bNetDirty )
|
|
CloakCharges;
|
|
}
|
|
|
|
simulated event ReplicatedEvent( name VarName )
|
|
{
|
|
if( VarName == nameOf(CloakCharges) )
|
|
{
|
|
if( IsLocallyControlled() )
|
|
{
|
|
UpdateCloakCharges();
|
|
}
|
|
return;
|
|
}
|
|
|
|
super.ReplicatedEvent( VarName );
|
|
}
|
|
|
|
function PossessedBy( Controller C, bool bVehicleTransition )
|
|
{
|
|
super.PossessedBy(C, bVehicleTransition);
|
|
|
|
class'KFPawn_MonsterBoss'.static.PlayBossEntranceTheatrics(self);
|
|
MyKFPC = KFPlayerController(C);
|
|
|
|
// Start the cloak timer
|
|
SetTimer( 2.f + fRand(), false, nameOf(Timer_EnableCloak) );
|
|
}
|
|
|
|
/** Toggle cloaking material */
|
|
function SetCloaked(bool bNewCloaking)
|
|
{
|
|
if( bNewCloaking && (IsDoingSpecialMove() || CloakCharges == 0) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
super.SetCloaked(bNewCloaking);
|
|
|
|
UpdateCloakedTimer();
|
|
|
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
|
{
|
|
UpdateCloakCharges();
|
|
}
|
|
}
|
|
|
|
/** Starts or stops our cloaking timer */
|
|
function UpdateCloakedTimer()
|
|
{
|
|
if( CloakCharges == 0 || !bIsCloaking )
|
|
{
|
|
if( IsTimerActive(nameOf(Timer_UpdateCloakCharge)) )
|
|
{
|
|
ClearTimer( nameOf(Timer_UpdateCloakCharge) );
|
|
}
|
|
return;
|
|
}
|
|
|
|
if( bIsCloaking )
|
|
{
|
|
if( !IsTimerActive(nameOf(Timer_UpdateCloakCharge)) )
|
|
{
|
|
SetTimer( 1.f, true, nameOf(Timer_UpdateCloakCharge) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Runs every second to tick off our cloak charges */
|
|
function Timer_UpdateCloakCharge()
|
|
{
|
|
CloakCharges = Max( CloakCharges - 1, 0 );
|
|
|
|
if( CloakCharges == 0 )
|
|
{
|
|
SetCloaked( false );
|
|
ClearTimer( nameOf(Timer_UpdateCloakCharge) );
|
|
}
|
|
|
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
|
{
|
|
UpdateCloakCharges();
|
|
}
|
|
}
|
|
|
|
/** Updates the number of cloak charges for UI */
|
|
private function UpdateCloakCharges()
|
|
{
|
|
SpecialMoveCooldowns[7].Charges = CloakCharges;
|
|
}
|
|
|
|
/** Turns on the cloak after a specified amount of time */
|
|
function Timer_EnableCloak()
|
|
{
|
|
SetCloaked( true );
|
|
}
|
|
|
|
/** Update our barrel spin skel control */
|
|
simulated event Tick( float DeltaTime )
|
|
{
|
|
if( WorldInfo.NetMode != NM_DedicatedServer )
|
|
{
|
|
// Cache off our viewer player
|
|
if( ViewerPlayer == none )
|
|
{
|
|
ViewerPlayer = KFPlayerController( WorldInfo.GetALocalPlayerController() );
|
|
}
|
|
|
|
UpdateHealAvailable();
|
|
UpdateCloakIconState();
|
|
}
|
|
|
|
super.Tick( DeltaTime );
|
|
}
|
|
|
|
/** Updates our gun tracking skeletal control */
|
|
simulated function UpdateGunTrackingSkelCtrl( float DeltaTime )
|
|
{
|
|
local rotator ViewRot;
|
|
|
|
// Track the player with the gun arm
|
|
if( GunTrackingSkelCtrl != none )
|
|
{
|
|
if( bGunTracking )
|
|
{
|
|
ViewRot = GetViewRotation();
|
|
if( Role < ROLE_Authority && !IsLocallyControlled() )
|
|
{
|
|
ViewRot.Pitch = NormalizeRotAxis( RemoteViewPitch << 8 );
|
|
}
|
|
GunTrackingSkelCtrl.DesiredTargetLocation = GetPawnViewLocation() + vector(ViewRot) * 5000.f;
|
|
GunTrackingSkelCtrl.InterpolateTargetLocation( DeltaTime );
|
|
}
|
|
else
|
|
{
|
|
GunTrackingSkelCtrl.SetSkelControlActive( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Gets the minimum cloaked amount based on the viewer */
|
|
simulated protected function float GetMinCloakPct()
|
|
{
|
|
if( ViewerPlayer != none && (ViewerPlayer.GetTeamNum() == GetTeamNum() || ViewerPlayer.PlayerReplicationInfo.bOnlySpectator) )
|
|
{
|
|
return 0.4f;
|
|
}
|
|
|
|
return super.GetMinCloakPct();
|
|
}
|
|
|
|
/** Updates the HUD interaction message and heal icon to show current heal capability */
|
|
private function UpdateHealAvailable()
|
|
{
|
|
if( !IsHealAllowed() )
|
|
{
|
|
//extend the cooldown of heal here
|
|
SpecialMoveCooldowns[5].LastUsedTime = WorldInfo.TimeSeconds;
|
|
}
|
|
}
|
|
|
|
/** Updates the state of the cloaking icon */
|
|
private function UpdateCloakIconState()
|
|
{
|
|
if( SpecialMoveCooldowns[7].Charges == 0 )
|
|
{
|
|
SpecialMoveCooldowns[7].LastUsedTime = WorldInfo.TimeSeconds;
|
|
}
|
|
else
|
|
{
|
|
SpecialMoveCooldowns[7].LastUsedTime = 0;
|
|
}
|
|
}
|
|
|
|
/** If true, we have enough heal charges remaining to execute a heal */
|
|
private function bool IsHealAllowed()
|
|
{
|
|
return (GetHealthPercentage() < HealThreshold && SpecialMoveCooldowns[5].Charges > 0);
|
|
}
|
|
|
|
/** Retrieves the aim direction and target location for each missile. Called from SpecialMove */
|
|
function GetMissileAimDirAndTargetLoc( int MissileNum, vector MissileLoc, rotator MissileRot, out vector AimDir, out vector TargetLoc )
|
|
{
|
|
local PlayerController PC;
|
|
local vector HitLocation, HitNormal;
|
|
local vector TraceStart, TraceEnd;
|
|
local Actor HitActor;
|
|
|
|
PC = PlayerController(Controller);
|
|
if( PC == none )
|
|
{
|
|
return;
|
|
}
|
|
|
|
TraceStart = PC.PlayerCamera.CameraCache.POV.Location;
|
|
TraceEnd = PC.PlayerCamera.CameraCache.POV.Location + vector(PC.PlayerCamera.CameraCache.POV.Rotation)*10000.f;
|
|
|
|
HitActor = Trace( HitLocation, HitNormal, TraceEnd, TraceStart, TRUE,,, TRACEFLAG_Bullet );
|
|
|
|
if( HitActor != none )
|
|
{
|
|
AimDir = Normal(HitLocation - MissileLoc);
|
|
TargetLoc = HitLocation;
|
|
}
|
|
else
|
|
{
|
|
AimDir = Normal( TraceEnd - MissileLoc);
|
|
TargetLoc = TraceEnd;
|
|
}
|
|
}
|
|
|
|
/** Retrieves the aim direction and target location for each mortar. Called from SpecialMove */
|
|
function GetMortarAimDirAndTargetLoc( int MissileNum, vector MissileLoc, rotator MissileRot, out vector AimDir, out vector TargetLoc, out float MissileSpeed )
|
|
{
|
|
local Patriarch_MortarTarget MissileTarget;
|
|
local vector X,Y,Z;
|
|
|
|
GetAxes( MissileRot, X,Y,Z );
|
|
|
|
// Each missile can possibly target a separate player
|
|
MissileTarget = GetMortarTarget(MissileNum);
|
|
|
|
// Aim at the feet
|
|
TargetLoc = MissileTarget.TargetPawn.Location + (vect(0,0,-1)*MissileTarget.TargetPawn.GetCollisionHeight());
|
|
|
|
// Nudge the spread a tiny bit to make the missiles less concentrated on a single point
|
|
AimDir = Normal( vect(0,0,1) + Normal(MissileTarget.TargetVelocity) );
|
|
|
|
// Set the missile speed
|
|
MissileSpeed = VSize( MissileTarget.TargetVelocity );
|
|
}
|
|
|
|
/** Allows pawn to do any pre-mortar attack prep */
|
|
function PreMortarAttack()
|
|
{
|
|
ClearMortarTargets();
|
|
CollectMortarTargets( true, true );
|
|
CollectMortarTargets();
|
|
}
|
|
|
|
/** Tries to set our mortar targets */
|
|
function bool CollectMortarTargets( optional bool bInitialTarget, optional bool bForceInitialTarget )
|
|
{
|
|
local int NumTargets;
|
|
local KFPawn_Human KFP;
|
|
local float TargetDistSQ;
|
|
local vector MortarVelocity, MortarStartLoc, TargetLoc, TargetProjection;
|
|
|
|
MortarStartLoc = Location + vect(0,0,1)*GetCollisionHeight();
|
|
NumTargets = bInitialTarget ? 0 : 1;
|
|
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFP )
|
|
{
|
|
if( !KFP.IsAliveAndWell() || MortarTargets.Find('TargetPawn', KFP) != INDEX_NONE )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Make sure target is in range
|
|
TargetLoc = KFP.Location + (vect(0,0,-1)*(KFP.GetCollisionHeight()*0.8f));
|
|
TargetProjection = MortarStartLoc - TargetLoc;
|
|
TargetDistSQ = VSizeSQ( TargetProjection );
|
|
if( TargetDistSQ > MinMortarRangeSQ && TargetDistSQ < MaxMortarRangeSQ )
|
|
{
|
|
TargetLoc += Normal(TargetProjection)*KFP.GetCollisionRadius();
|
|
if( SuggestTossVelocity(MortarVelocity, TargetLoc, MortarStartLoc, MortarProjectileClass.default.Speed, 500.f, 1.f, vect(0,0,0),, GetGravityZ()*0.8f) )
|
|
{
|
|
// Make sure upward arc path is clear
|
|
if( !FastTrace(MortarStartLoc + (Normal(vect(0,0,1) + (Normal(TargetLoc - MortarStartLoc)*0.9f))*fMax(VSize(MortarVelocity)*0.55f, 800.f)), MortarStartLoc,, true) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MortarTargets.Insert( NumTargets, 1 );
|
|
MortarTargets[NumTargets].TargetPawn = KFP;
|
|
MortarTargets[NumTargets].TargetVelocity = MortarVelocity;
|
|
|
|
if( bInitialTarget || NumTargets == 2 )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
NumTargets++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Clears mortar targets */
|
|
function ClearMortarTargets()
|
|
{
|
|
MortarTargets.Length = 0;
|
|
}
|
|
|
|
/** Increment Patriarch to the next battle phase */
|
|
function IncrementBattlePhase()
|
|
{
|
|
super.IncrementBattlePhase();
|
|
|
|
bSummonedThisPhase = false;
|
|
}
|
|
|
|
/** Check health percentage to see if we should summon children or allow healing */
|
|
private function CheckHealth()
|
|
{
|
|
local float HealthPct;
|
|
|
|
HealthPct = GetHealthPercentage();
|
|
|
|
if( HealthPct < HealThreshold )
|
|
{
|
|
if( Role == ROLE_Authority )
|
|
{
|
|
//SummonChildren();
|
|
|
|
// Perform an autoheal if necessary
|
|
if( SpecialMoveCooldowns[5].Charges > 0 && HealthPct <= AutoHealThreshold )
|
|
{
|
|
if( IsDoingSpecialMove() && !IsDoingSpecialMove(SM_PlayerZedMove_Q) )
|
|
{
|
|
EndSpecialMove();
|
|
}
|
|
bAutoHealed = true;
|
|
DoSpecialMove( SM_PlayerZedMove_Q, true );
|
|
}
|
|
}
|
|
|
|
if( !bWarnedLowHealthThisPhase && IsLocallyControlled() && MyKFPC.MyGFxHUD != none && HealthPct <= LowHealthThreshold && SpecialMoveCooldowns[5].Charges > 0 )
|
|
{
|
|
bWarnedLowHealthThisPhase = true;
|
|
MyKFPC.MyGFxHUD.ShowNonCriticalMessage( LowHealthMsg );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called from KFSpecialMove::SpecialMoveEnded */
|
|
simulated function NotifySpecialMoveEnded( KFSpecialMove FinishedMove, ESpecialMove SMHandle )
|
|
{
|
|
super.NotifySpecialMoveEnded( FinishedMove, SMHandle );
|
|
|
|
if( Role == ROLE_Authority )
|
|
{
|
|
SetTimer( 2.f + fRand(), false, nameOf(Timer_EnableCloak) );
|
|
}
|
|
}
|
|
|
|
/** Overriden to set the Patriarch to feel mode when his health passes a certain threshold */
|
|
function NotifyTakeHit(Controller InstigatedBy, vector HitLocation, int Damage, class<DamageType> DamageType, vector Momentum, Actor DamageCauser)
|
|
{
|
|
Super.NotifyTakeHit(InstigatedBy, HitLocation, Damage, DamageType, Momentum, DamageCauser);
|
|
|
|
CheckHealth();
|
|
}
|
|
|
|
/** Notification from the heal specialmove that we performed a successful heal */
|
|
function NotifyHealed()
|
|
{
|
|
// Reset low health warning
|
|
bWarnedLowHealthThisPhase = false;
|
|
|
|
// Reduce our number of heals by 1
|
|
--SpecialMoveCooldowns[5].Charges;
|
|
|
|
// Reset our cloak charges
|
|
CloakCharges = bAutoHealed ? byte( float(default.CloakCharges) * 0.75f ) : default.CloakCharges;
|
|
SpecialMoveCooldowns[7].Charges = CloakCharges;
|
|
|
|
// Reset autoheal status
|
|
bAutoHealed = false;
|
|
}
|
|
|
|
/** Allow humans to draw a positional icon to find us when we're uncloaked */
|
|
simulated function bool ShouldDrawBossIcon()
|
|
{
|
|
return !(bIsCloaking);
|
|
}
|
|
|
|
/** Returns TRUE if we're aiming with the husk cannon */
|
|
simulated function bool UseAdjustedControllerSensitivity()
|
|
{
|
|
return IsDoingSpecialMove( SM_PlayerZedMove_RMB ) || IsDoingSpecialMove( SM_PlayerZedMove_MMB );
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Zed Summoning
|
|
**********************************************************************************************/
|
|
|
|
/** Summon some children */
|
|
singular function SummonChildren()
|
|
{
|
|
if( !bSummonedThisPhase )
|
|
{
|
|
super.SummonChildren();
|
|
|
|
bSummonedThisPhase = true;
|
|
SetTimer( SummonChildrenDuration, false, nameOf(Timer_StopSummoningChildren) );
|
|
|
|
SetFleeAndHealMode( true );
|
|
}
|
|
}
|
|
|
|
/** Stop summoning children */
|
|
function Timer_StopSummoningChildren()
|
|
{
|
|
KFGameInfo(WorldInfo.Game).SpawnManager.StopSummoningBossMinions();
|
|
}
|
|
|
|
DefaultProperties
|
|
{
|
|
bVersusZed=true
|
|
TeammateCollisionRadiusPercent=0.30
|
|
|
|
XPValues(0)=2500 // 2x default because you have to play two rounds
|
|
|
|
bUseServerSideGunTracking=true
|
|
bNeedsCrosshair=true
|
|
SummonChildrenDuration=10.f
|
|
HealThreshold=0.5f
|
|
AutoHealThreshold=0.25f
|
|
LowHealthThreshold=0.3f
|
|
Health=1680 //2540 //2240
|
|
|
|
SprintSpeed=700.f
|
|
SprintStrafeSpeed=400.f
|
|
GroundSpeed=260.f
|
|
|
|
Begin Object Name=MeleeHelper_0
|
|
BaseDamage=25.f //45.f //40.0
|
|
MaxHitRange=375.f
|
|
MomentumTransfer=40000.f
|
|
MyDamageType=class'KFDT_Bludgeon_Patriarch'
|
|
End Object
|
|
MeleeAttackHelper=MeleeHelper_0
|
|
|
|
DefaultInventory(0)=class'KFWeap_Minigun_Patriarch_Versus'
|
|
|
|
Begin Object Name=SpecialMoveHandler_0
|
|
SpecialMoveClasses(SM_PlayerZedMove_LMB)=class'KFSM_PlayerPatriarch_Melee'
|
|
SpecialMoveClasses(SM_PlayerZedMove_RMB)=class'KFSM_PlayerPatriarch_MinigunBarrage'
|
|
SpecialMoveClasses(SM_PlayerZedMove_V)=class'KFSM_PlayerPatriarch_TentacleGrab'
|
|
SpecialMoveClasses(SM_PlayerZedMove_MMB)=class'KFSM_PlayerPatriarch_MissileAttack'
|
|
SpecialMoveClasses(SM_PlayerZedMove_Q)=class'KFSM_PlayerPatriarch_Heal'
|
|
SpecialMoveClasses(SM_PlayerZedMove_G)=class'KFSM_PlayerPatriarch_MortarAttack'
|
|
SpecialMoveClasses(SM_Taunt)=class'KFSM_Patriarch_Taunt'
|
|
End Object
|
|
|
|
MoveListGamepadScheme(ZGM_Attack_R2)=SM_PlayerZedMove_RMB
|
|
MoveListGamepadScheme(ZGM_Attack_L2)=SM_PlayerZedMove_MMB
|
|
MoveListGamepadScheme(ZGM_Melee_Square)=SM_PlayerZedMove_LMB
|
|
MoveListGamepadScheme(ZGM_Melee_Triangle)=SM_PlayerZedMove_V
|
|
MoveListGamepadScheme(ZGM_Block_R1)=SM_PlayerZedMove_Q
|
|
MoveListGamepadScheme(ZGM_Explosive_Ll)=SM_PlayerZedMove_G
|
|
MoveListGamepadScheme(ZGM_Special_R3)=SM_PlayerZedMove_V
|
|
|
|
// Gun stance cooldowns
|
|
SpecialMoveCooldowns(0)=(SMHandle=SM_PlayerZedMove_LMB, CooldownTime=0.5f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Generic-HeavyMelee', NameLocalizationKey="Melee")
|
|
SpecialMoveCooldowns(1)=(SMHandle=SM_PlayerZedMove_RMB, CooldownTime=4.0f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Patriarch-MiniGun', NameLocalizationKey="Minigun")
|
|
SpecialMoveCooldowns(2)=(SMHandle=SM_Taunt, CooldownTime=0.0f, bShowOnHud=false))
|
|
SpecialMoveCooldowns(3)=(SMHandle=SM_PlayerZedMove_V, CooldownTime=5.0f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Patriarch-TentacleAttack', NameLocalizationKey="Grab")
|
|
SpecialMoveCooldowns(4)=(SMHandle=SM_PlayerZedMove_MMB, CooldownTime=5.0f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Patriarch-Rocket', NameLocalizationKey="Rocket")
|
|
SpecialMoveCooldowns(5)=(SMHandle=SM_PlayerZedMove_Q, CooldownTime=6.f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Patriarch-Heal', Charges=3,NameLocalizationKey="Heal")
|
|
SpecialMoveCooldowns(6)=(SMHandle=SM_PlayerZedMove_G, CooldownTime=2.35f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Patriarch-MortarStrike', NameLocalizationKey="Mortar")
|
|
SpecialMoveCooldowns(7)=(SMHandle=SM_None, CooldownTime=999.f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Generic-Cloak', Charges=60)
|
|
SpecialMoveCooldowns.Add((SMHandle=SM_Jump, CooldownTime=1.0f, SpecialMoveIcon=Texture2D'ZED_Patriarch_UI.ZED-VS_Icons_Generic-Jump', bShowOnHud=false)) // Jump always at end of array
|
|
|
|
BattlePhases(0)={(HealAmounts={(1.0f)},
|
|
TentacleDamage=10,
|
|
bCanSummonMinions=true)}
|
|
BattlePhases(1)={(HealAmounts={(1.0f)},
|
|
TentacleDamage=10,
|
|
bCanSummonMinions=true)}
|
|
BattlePhases(2)={(HealAmounts={(0.9f)},
|
|
TentacleDamage=10,
|
|
bCanSummonMinions=true)}
|
|
BattlePhases(3)={(TentacleDamage=10,
|
|
bCanSummonMinions=true)}
|
|
|
|
MissileProjectileClass=class'KFProj_Missile_Patriarch_Versus'
|
|
MortarProjectileClass=class'KFProj_Mortar_Patriarch'
|
|
|
|
MinMortarRangeSQ=160000.f
|
|
MaxMortarRangeSQ=6250000.f
|
|
|
|
CloakCharges=60
|
|
|
|
|
|
|
|
IncapSettings(AF_Snare)= (Vulnerability=(0.7, 0.7, 1.0, 0.7), Cooldown=8.5, Duration=1.5)
|
|
|
|
|
|
//defaults
|
|
ThirdPersonViewOffset={(
|
|
OffsetHigh=(X=-200,Y=90,Z=45),
|
|
OffsetLow=(X=-220,Y=130,Z=55),
|
|
OffsetMid=(X=-185,Y=110,Z=45),
|
|
)}
|
|
}
|