/** * Jank by Starblaster64 and m_cu8 * This code is just a big trainwreck... (haha, get it? :hueh:) */ class sb64_Hazard_CatTrain extends mcu8_Hazard_Train; // train cat running v fast var(CatTrainNative) vector2d SpeedNodeMinMaxRate; var(CatTrainNative) Name AimNodeTrainBoneNameA; var(CatTrainNative) Name AimNodeTrainBoneNameB; // In Degrees!!! var(CatTrainNative) vector2d AimNodeMinMaxPitch; // In Degrees!!! var(CatTrainNative) vector2d AimNodeMinMaxYaw; // Name of TrainMesh bone that CatMesh aligns with. var(CatTrainNative) Name CatMeshTrainBoneName; var(CatTrainNative) vector CatMeshOffset; var(CatTrainNative) vector TrainMeshOffset; var(CatTrainNative) vector CatCollisionOffset; var(CatTrainNative) rotator CatCollisionRotationOffset; // Name of TrainMesh bone that TrainFront collision mesh aligns with. var(CatTrainNative) Name TrainFrontHitBoneName; var(CatTrainNative) vector TrainFrontHitOffset; var(CatTrainNative) rotator TrainFrontHitRotationOffset; // MCU8-ADD: from original Hat_Hazard_CatTrain const HeadlightDistanceThreshold = 2500.f; const HeadlightFadeTime = 0.7f; const DebugNeverOptimize = true; const DebugAlwaysShowCollisionBoxes = false; // Spawns additional cat trains with the same settings on the same track var(CatTrain) int SpawnDuplicates; // The skeletal mesh attached to this train. var(CatTrain) SkeletalMeshComponent TrainMesh; // Skeletal mesh of the cat that leads the train. var(CatTrain) SkeletalMeshComponent CatMesh; // If enabled, non-player objects may fall through trains, but performance will improve. var(CatTrain) bool OptimizeTrainCollisions; // Collision geometry of the cat itself. var StaticMeshComponent CatCollision; // Collision geometry of the front of the train. var StaticMeshComponent TrainFrontHit; // How many cars in this train. var int CarsInTrain; // How many cars (lengthwise) each "car" is made up of, allowing cars to "bend". Decrease this value to improve performance for trains that barely turn, or trains where you don't touch the sides or top. var int CarCollisionDensity; // Size of the collision box for each car. Don't change this unless you change the mesh! var Vector CarCollisionSize; // Whether to render the collision boxes. var(Debug) bool DebugRenderCollisionBoxes; // How many "cars" long the cat is. This area is intangible aside from the cat hitbox. var float FrontClip; // How many "cars" should be intangible at the end of the train. var float RearClip; // The names of the single-bone skeletal controls in the mesh's AnimTree. var Array AnimTreeControls; // Distance between bones. Should all be equal. var float BoneDensity; // How much the bones tuck inwards when going around corners. Use this if the bone weights don't line up with the collision boxes when cornering. var float AverageCorners; // The spotlight attached to the front of the train. // Aruki note: This should be removed, I'm only leaving it to preserve the designer-set bEnabled value for now var(CatTrain) SpotLightComponent Headlight; // The spotlight actor attached to the front of the train. var(CatTrain) bool bEnableDynamicHeadlight; var transient Hat_TrainHeadlight HeadlightActor; var float CatAnimRunSpeed; var Array TrainBones; var AnimNodeAimOffset AimNode; var AnimNodeScalePlayRate SpeedNode; var(CatTrain) SpriteComponent SpriteEyes[2]; var(CatTrain) SpriteComponent SpriteHeadlights[2]; var transient Vector LastBoneUpdatePos; var transient bool CatMidTeleport; var transient bool DebugDontUpdateViewOptimize; var transient float HeadlightOpacity; var(Sounds) SoundCue FootStepSound; var(Sounds) SoundCue HoloStepSound; var(Sounds) SoundCue MeowVoiceClip; var(Sounds) Vector2D VoiceInterval; // MCU8-ADD-END simulated event PostBeginPlay() { Super.PostBeginPlay(); SetTimer(0.001, false, NameOf(PostPostBeginPlay)); if (!IsDweller) SetTrainColor(); if (SpriteEyes[0] != None && CatMesh != None) { SpriteEyes[0].SetHidden(false); CatMesh.AttachComponentToSocket(SpriteEyes[0], 'EyeL'); } if (SpriteEyes[1] != None && CatMesh != None) { SpriteEyes[1].SetHidden(false); CatMesh.AttachComponentToSocket(SpriteEyes[1], 'EyeR'); } if (SpriteHeadlights[0] != None && TrainMesh != None) { SpriteHeadlights[0].SetHidden(false); TrainMesh.AttachComponentToSocket(SpriteHeadlights[0], 'HeadLight'); } if (SpriteHeadlights[1] != None && TrainMesh != None) { SpriteHeadlights[1].SetHidden(false); TrainMesh.AttachComponentToSocket(SpriteHeadlights[1], 'BackLight'); } if (DebugRenderCollisionBoxes) { CatCollision.SetHidden(false); TrainFrontHit.SetHidden(false); } } function PostPostBeginPlay() { local mcu8_Hazard_Train car; local bool SetFrontDamage; // doesn't work, peck it //`assert(TickOptimizationGroup == StartPoint.TrackMaster.TickOptimizationGroup);// note: if this pops up, recalculate tracks! if (SpawnDuplicates > 0 && InitDuplicates()) return; if (StartPoint.TrackMaster.TrackIsStraight) CarCollisionDensity = 1; CarLength = CarCollisionSize.X/float(CarCollisionDensity); CarHeight = CarCollisionSize.Z/2; CarCollisionSize.X *= 1.3;// some overlap makes covering bends better if (TrainMesh == None || DebugAlwaysShowCollisionBoxes) DebugRenderCollisionBoxes = true; if (DebugRenderCollisionBoxes) { StaticMeshComponent.SetHidden(false); StaticMeshComponent.SetMaterial(0, Material'HatInTime_ScienceOwlAssets.Materials.Laser_outline'); bDebugVisualizePlayerBoundsCheck = true; } while (Train.Length > 0) { if (Train[Train.Length-1] != self) Train[Train.Length-1].Destroy(); Train.Remove(Train.Length-1, 1); } Train.AddItem(self); StaticMeshComponent.SetScale3D(CarCollisionSize*vect(0,1,1)/256.f + CarCollisionSize*vect(1,0,0)/float(CarCollisionDensity)/256.f); while (CarsInTrain > 0 && Train.Length < CarsInTrain*CarCollisionDensity) { car = Spawn(class'mcu8_Hazard_Train', self,, Location, Rotation); car.Following = Train[Train.Length-1]; car.FollowFrontDirectly = true; car.CollisionComponent.CanBeEdgeGrabbed = CollisionComponent.CanBeEdgeGrabbed && CollisionComponent.CanBeStoodOn; car.CollisionComponent.CanBeWallSlid = CollisionComponent.CanBeWallSlid && CollisionComponent.CanBeStoodOn; car.CollisionComponent.CanBeStoodOn = CollisionComponent.CanBeStoodOn; if (IsDweller) { car.IsDweller = true; car.InitialBlockActors = sb64_Hazard_CatTrain_Dweller(Self).InitialBlockActors; car.InvertCollisionWhenDeactivated_Internal = sb64_Hazard_CatTrain_Dweller(Self).InvertCollisionWhenDeactivated; } car.CollisionComponent.CanClimbInfinitely = true; car.bPlayerShouldStayBasedAfterJumping = bPlayerShouldStayBasedAfterJumping; car.OrientPlayerCamera = OrientPlayerCamera; car.TurnCameraToFaceTrainDirectionOnBase = TurnCameraToFaceTrainDirectionOnBase; car.TurnCameraEvenIfPreviousBaseWasTrain = TurnCameraEvenIfPreviousBaseWasTrain; if (HiddenVolumeList.Length > 0) car.SetHiddenVolumeList(HiddenVolumeList); if (DebugRenderCollisionBoxes) car.bDebugVisualizePlayerBoundsCheck = true; if (Train.Length <= Round(FrontClip*CarCollisionDensity) || Train.Length >= Round(CarsInTrain - RearClip)*CarCollisionDensity) { car.SetCollision(false, false); car.StaticMeshComponent.SetStaticMesh(None); car.StaticMeshComponent.SetHidden(true); car.DetachComponent(car.StaticMeshComponent); } else { if (!SetFrontDamage && 1 <= Round(FrontClip*CarCollisionDensity)) { car.IsDamageFront = true; SetFrontDamage = true; } car.StaticMeshComponent.SetStaticMesh(StaticMeshComponent.StaticMesh); car.StaticMeshComponent.SetScale3D(StaticMeshComponent.Scale3D); if (StaticMeshComponent.HiddenGame) car.StaticMeshComponent.SetHidden(true); else car.StaticMeshComponent.SetMaterial(0, StaticMeshComponent.Materials[0]); } car.CarLength = CarLength; car.CarHeight = CarHeight; `NPCManager.NPCManager.SetActorTickOptimizationGroup(car, TickOptimizationGroup); car.InitTrain(Train.Length%CarCollisionDensity == 0); } if (1 <= Round(FrontClip*CarCollisionDensity)) { StaticMeshComponent.SetActorCollision(false, false); //DetachComponent(StaticMeshComponent); //StaticMeshComponent = None; StaticMeshComponent.SetHidden(true); } SetTimer(RandRange(VoiceInterval.X, VoiceInterval.Y), false, NameOf(PlayMeowSound)); if (Headlight.bEnabled || bEnableDynamicHeadlight) { Headlight.SetEnabled(false); HeadlightActor = Spawn(class'Hat_TrainHeadlight', self); HeadlightActor.SetHardAttach(true); HeadlightActor.SetBase(self); } } function InitTrain(bool DoInitShake = true) { Super.InitTrain(DoInitShake); if (TrainWheelSound != None) { TrainWheelSound.FadeIn(4,1); TrainWheelSound.PitchMultiplier = ((AdjustedSpeed / default.Speed) / 2) + 0.5f; if (TrainMesh != None) { if (TrainMesh.GetSocketByName('WheelsFront') != None) TrainWheelSound.AudioVectorsWS.Add(1); if (TrainMesh.GetSocketByName('WheelsMiddle') != None) TrainWheelSound.AudioVectorsWS.Add(1); if (TrainMesh.GetSocketByName('WheelsBack') != None) TrainWheelSound.AudioVectorsWS.Add(1); } } } function FadeOutTrainFrontSound() { if (TrainFrontSound != None) TrainFrontSound.FadeOut(0.3,0); } simulated event Tick(float d) { UpdateOptimization(); UpdateWheelAudioComponent(); // FUCK THIS // EDIT: LMAO THAT WORK... UpdateVisuals2(); Super.Tick(d); } function UpdateOptimization() { local bool NearTrainAtAll, AllowCollision, PlayerStandingOnTrain; local int i, BoneIndexThreshold, FirstTrainIndexP1, FirstTrainIndexP2, LastTrainIndexP1, LastTrainIndexP2; local float TrainBoneRatio; if (Train.Length == 0 || !Initialized) return; if (DebugNeverOptimize) return; if (!OptimizeTrainCollisions) { if (!IsPlayerNearby) for (i = 0; i < Train.Length; i++) Train[i].SetIsPlayerNearby(true, DebugRenderCollisionBoxes); return; } NearTrainAtAll = DetermineIsPlayerNearby(TrainMesh); TrainBoneRatio = 1.0/float(AnimTreeControls.Length-1)*(Train.Length); BoneIndexThreshold = (StartPoint.TrackMaster.TrackIsStraight ? 1 : 3); if (ClosestBoneIndices[0] >= 0) { FirstTrainIndexP1 = int(ClosestBoneIndices[0]*TrainBoneRatio); LastTrainIndexP1 = FirstTrainIndexP1 + BoneIndexThreshold; } else { FirstTrainIndexP1 = -1; LastTrainIndexP1 = -1; } if (ClosestBoneIndices[1] >= 0) { FirstTrainIndexP2 = int(ClosestBoneIndices[1]*TrainBoneRatio); LastTrainIndexP2 = FirstTrainIndexP2 + BoneIndexThreshold; } else { FirstTrainIndexP2 = -1; LastTrainIndexP2 = -1; } for (i = 0; i < Train.Length; i++) { // We want to always enable the train if it, or an adjacent train, has players standing on it PlayerStandingOnTrain = Train[i].PlayersBasedOnTrain.length > 0 || (i > 0 && Train[i-1].PlayersBasedOnTrain.length > 0) || (i < Train.length-1 && Train[i+1].PlayersBasedOnTrain.length > 0); if (PlayerStandingOnTrain) { Train[i].SetIsPlayerNearby(true, DebugRenderCollisionBoxes); } else if (Train[i] == self || !NearTrainAtAll) { Train[i].SetIsPlayerNearby(NearTrainAtAll, DebugRenderCollisionBoxes); } else { // only tick train cars nearest to hatkid and bowkid AllowCollision = ( i >= FirstTrainIndexP1 && i <= LastTrainIndexP1 ) || ( i >= FirstTrainIndexP2 && i <= LastTrainIndexP2 ) ; Train[i].SetIsPlayerNearby(AllowCollision && Train[i].StaticMeshComponent.StaticMesh != None, DebugRenderCollisionBoxes); } } } simulated event PostInitAnimTree(SkeletalMeshComponent SkelComp) { local Name n; local SkelControlSingleBone skel; Super.PostInitAnimTree(SkelComp); if (SkelComp == CatMesh) { AimNode = AnimNodeAimOffset(SkelComp.FindAnimNode('Aim')); SpeedNode = AnimNodeScalePlayRate(SkelComp.FindAnimNode('PlayRate')); } if (SkelComp == TrainMesh) { TrainBones.Length = 0; foreach AnimTreeControls(n) { skel = SkelControlSingleBone(TrainMesh.FindSkelControl(n)); skel.ControlStrength = 1; TrainBones.AddItem(skel); } } } simulated event PlayFootStepSound(int FootDown) { if (StartPoint != None && StartPoint.TrackMaster.SubPoints[CurrentSubPoint].bHasGeneratedTrack) { if (HoloStepSound != None) PlaySound(HoloStepSound); } else if (FootStepSound != None) PlaySound(FootStepSound); } function UpdateWheelAudioComponent() { local int i; local Vector loc; local Rotator rot; if (TrainWheelSound == None) return; if (TrainMesh == None) return; if (bHidden && TrainWheelSound.IsPlaying()) { TrainWheelSound.Stop(); return; } if (!TrainWheelSound.IsPlaying()) { TrainWheelSound.FadeIn(4,1); } if (!TrainWheelSound.IsPlaying()) return; if (TrainMesh.GetSocketByName('WheelsFront') != None) { if (TrainMesh.GetSocketWorldLocationAndRotation('WheelsFront', loc, rot, 0)) { TrainWheelSound.AudioVectorsWS[i] = loc; i++; } } if (TrainMesh.GetSocketByName('WheelsMiddle') != None) { if (TrainMesh.GetSocketWorldLocationAndRotation('WheelsMiddle', loc, rot, 0)) { TrainWheelSound.AudioVectorsWS[i] = loc; i++; } } if (TrainMesh.GetSocketByName('WheelsBack') != None) { if (TrainMesh.GetSocketWorldLocationAndRotation('WheelsBack', loc, rot, 0)) TrainWheelSound.AudioVectorsWS[i] = loc; } } function SetTrainColor() { if (StartPoint == None || TrainMesh == None) return; switch (StartPoint.GeneratedTrackColor) { case Green: TrainMesh.SetMaterial(0, MaterialInstanceConstant'HatInTime_Levels_Metro_K.Materials.train_metro_green'); break; case Yellow: TrainMesh.SetMaterial(0, MaterialInstanceConstant'HatInTime_Levels_Metro_K.Materials.train_metro_yellow'); break; case Purple: TrainMesh.SetMaterial(0, MaterialInstanceConstant'HatInTime_Levels_Metro_K.Materials.train_metro_purple'); break; default: if (TrainMesh.Materials.Length > 0) TrainMesh.SetMaterial(0, None); break; } } simulated event Destroyed() { // Anim node references must be cleared when destroying Actors for the garbage collector to catch them. // CatTrains are destroyed when initializing duplicates, so this is required. // Presumebly this is normally handled by native code. AimNode = None; SpeedNode = None; TrainBones.Length = 0; while (Train.Length > 0) { if (Train[Train.Length-1] != self) Train[Train.Length-1].Destroy(); Train.Remove(Train.Length-1, 1); } Super.Destroyed(); } function TickFollowers(float d) { local mcu8_Hazard_Train t; // don't tick followers if entire train isn't nearby if (!IsPlayerNearby) return; foreach Train(t) if (t != self) if (t.IsPlayerNearby || t.PlayersBasedOnTrain.length > 0) t.FollowerTick(d); } function bool InitDuplicates() { local int i; local sb64_Hazard_CatTrain dupe; local float tracksize, dist; if (SpawnDuplicates <= 0) return false; if (StartPoint.TrackMaster == None || StartPoint.TrackMaster.EditorTimeTrackCalculationState != TrainPointCalcState_Complete || StartPoint.TrackMaster.SubPoints.Length == 0) { `broadcast(self@"is on a track that isn't calculated! Move one of the track points connected to"@StartPoint); ScriptTrace(); return false; } tracksize = StartPoint.GetWholeTrackLength(true); if (tracksize <= 0) { SpawnDuplicates = 0; return false; } for (i = 0; i <= SpawnDuplicates; i++) { dupe = Spawn(class,,, Location, Rotation, self, true); if (IsDweller) { dupe.IsDweller = true; dupe.InitialBlockActors = sb64_Hazard_CatTrain_Dweller(Self).InitialBlockActors; sb64_Hazard_CatTrain_Dweller(dupe).InvertCollisionWhenDeactivated = sb64_Hazard_CatTrain_Dweller(Self).InvertCollisionWhenDeactivated; dupe.InvertCollisionWhenDeactivated_Internal = sb64_Hazard_CatTrain_Dweller(Self).InvertCollisionWhenDeactivated; } dupe.SpawnDuplicates = 0;// lmao 2 cat // yes I laugh 2 dupe.Initialized = false; dupe.Train.Length = 0; dupe.InitTrain(true); if (dupe.CatMesh != None && default.CatMesh != None) dupe.CatMesh.SetAnimTreeTemplate(default.CatMesh.AnimTreeTemplate);// randomize animations dist = tracksize*(i/float(SpawnDuplicates+1)); while (dist > 10) { dupe.AddSubPointDistanceBase(StartPoint.TrackMaster.SubPoints, FMin(100,dist), InstantTeleport); dist -= FMin(100,dist); } `if(`notdefined(FINAL_RELEASE)) if (dupe.HiddenVolumeList.Length != HiddenVolumeList.Length) { `broadcast("Error, dupe has incorrect HiddenVolumeList"); } if (dupe.TickOptimize != TickOptimize) { `broadcast("Error, dupe has incorrect TickOptimize"); } `endif } Destroy(); return true; } event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal) { // don't get hit by the cow catcher while jumping unless in a tunnel if (IgnoreFrontDamageWhileAirborne && !IsNearerToCatThanTrainFront(Other) && CollisionComponent.CanBeStoodOn == true && Other.Physics != PHYS_Walking) return; Super.Touch(Other, OtherComp, HitLocation, HitNormal); } function bool IsNearerToCatThanTrainFront(Actor a) { // the Touch event cant differentiate between these, so just check which is nearest return VSizeSq((CatCollision.Translation >> Rotation) + Location - a.Location) < VSizeSq((TrainFrontHit.Translation >> Rotation) + Location - a.Location); } function TrainHit(Hat_PawnCombat p, Vector HitNormal) { if (Initialized && !Teleporting) Super.TrainHit(p, HitNormal); } event editoronly CheckForErrors(out Array ErrorMessages) { Super.CheckForErrors(ErrorMessages); if (DrawScale3D != vect(1,1,1) || DrawScale != 1) ErrorMessages.AddItem("Doesn't support manual scaling! Use CarCollisionSize instead."); } event editoronly OnEditorPropertyChanged(Name PropertyName) { Super.OnEditorPropertyChanged(PropertyName); if (PropertyName == 'SpawnDuplicates') if (StartPoint != None) StartPoint.PerformEditorTimeTrackCalculation(); } event editoronly name GetCustomTickOptimizationGroup() { if (StartPoint != None) return StartPoint.GetCustomTickOptimizationGroup(); } event editoronly GetCustomEditorCommands(out Array Result) { local ActorCustomEditorCommand Item; Item.Text = "Enable Dynamic Headlight (EXPENSIVE)"; Item.Options.Length = 0; Item.Command = 'ToggleDynamicHeadlight'; Item.CurrentValue = bEnableDynamicHeadlight ? 1 : 0; Result.AddItem(Item); } event editoronly OnEditorCustomCommand(Name Command, int Index) { if (Command == 'ToggleDynamicHeadlight') { bEnableDynamicHeadlight = !bEnableDynamicHeadlight; } } function PlayMeowSound(optional bool Stop) { if (!Stop) { PlaySound(MeowVoiceClip); SetTimer(RandRange(VoiceInterval.X, VoiceInterval.Y), false, NameOf(PlayMeowSound)); } } function UpdateVisuals2() { local mcu8_Hazard_Train t; if (TrainMesh == None) return; if (Train.Length == 0 || !Initialized) return;// not initialized yet if (Teleporting) { // don't stretch out the cats on teleport (yes it was very funny looking) TrainMesh.SetHidden(true); if (CatMesh != None) CatMesh.SetHidden(true); foreach Train(t) t.SetLocation(Location); //return; } else { if (TrainMesh != None) TrainMesh.SetHidden(false); if (CatMesh != None) CatMesh.SetHidden(false); } UpdateTrainMesh(); UpdateCatMesh(); UpdateCollisionMeshes(); //Super.UpdateVisuals(SubPoints); } // Mismatch with collision boxes becomes noticable on tight corners and inclines. // TODO: Figure out how to implement 'AverageCorners' variable. function UpdateTrainMesh() { local int i; local vector boneLocation; local rotator boneRotation; if (TrainMesh == None) return; if (StartPoint == None || StartPoint.TrackMaster == None) return; for (i = 0; i < TrainBones.Length; i++) { // Clamping because passing Distance value of exactly 0 to GetSubPointOffsetBase() always returns (0,0,0). boneLocation = GetSubPointOffsetBase(StartPoint.TrackMaster.SubPoints, FMax(0.001, BoneDensity * i) * -1, true, boneRotation); boneLocation += vect(0,0,1) * CarHeight + TrainMeshOffset; // boneLocation += (vect(0,0,1) << boneRotation) * CarHeight + TrainMeshOffset; // results were a bit wonk // Un-invert and adjust rotations boneRotation.Pitch *= -1; boneRotation.Yaw += `HalfRot; boneRotation.Roll += `QuarterRot; TrainBones[i].BoneTranslation = boneLocation; TrainBones[i].BoneRotation = boneRotation; // if (bDebug) `Broadcast(self @ "Bone" @ i $ ":" @ `ShowVar(boneLocation) @ "|" @ `ShowVar(self.Location)); } // AverageCorners // Affects whole train (except first and last bones). // Lower bound is 0, no upper bound. } // Not quite accurate to native's results, but seems to follow corners slightly better. function UpdateCatMesh() { local vector catLocation; local rotator aimRotation, boneRotation, boneRotation2; if (CatMesh == None) return; if (StartPoint == None || StartPoint.TrackMaster == None) return; if (TrainMesh != None && TrainMesh.MatchRefBone(CatMeshTrainBoneName) != INDEX_NONE) { catLocation = TrainMesh.GetBoneLocation(CatMeshTrainBoneName); aimRotation = QuatToRotator(TrainMesh.GetBoneQuaternion(CatMeshTrainBoneName)); aimRotation.Pitch *= -1; aimRotation.Yaw += `HalfRot; aimRotation.Roll -= `QuarterRot; } else { catLocation = GetSubPointOffsetBase(StartPoint.TrackMaster.SubPoints, -BoneDensity, true, aimRotation); } catLocation += CatMeshOffset >> aimRotation; CatMesh.SetTranslation(catLocation); CatMesh.SetRotation(aimRotation); CatMesh.SetAbsolute(true, true); if (AimNode != None) { boneRotation = Rotation; boneRotation2 = CatMesh.Rotation; if (TrainMesh != None) { if (TrainMesh.MatchRefBone(AimNodeTrainBoneNameA) != INDEX_NONE) { boneRotation = QuatToRotator(TrainMesh.GetBoneQuaternion(AimNodeTrainBoneNameA)); boneRotation.Pitch *= -1; boneRotation.Yaw -= `HalfRot; boneRotation.Roll -= `QuarterRot; } if (TrainMesh.MatchRefBone(AimNodeTrainBoneNameB) != INDEX_NONE) { boneRotation2 = QuatToRotator(TrainMesh.GetBoneQuaternion(AimNodeTrainBoneNameB)); boneRotation2.Pitch *= -1; boneRotation2.Yaw -= `HalfRot; boneRotation2.Roll -= `QuarterRot; } } aimRotation = Normalize(boneRotation2 - boneRotation); aimRotation.Pitch = Clamp(aimRotation.Pitch, AimNodeMinMaxPitch.X * DegToUnrRot, AimNodeMinMaxPitch.Y * DegToUnrRot); aimRotation.Yaw = Clamp(aimRotation.Yaw, AimNodeMinMaxYaw.X * DegToUnrRot, AimNodeMinMaxYaw.Y * DegToUnrRot); // Pitch AimNode.Aim.Y = 0.0; if (aimRotation.Pitch != 0) AimNode.Aim.Y = aimRotation.Pitch / ((aimRotation.Pitch < 0 ? AimNodeMinMaxPitch.X : AimNodeMinMaxPitch.Y) * DegToUnrRot); // Yaw AimNode.Aim.X = 0.0; if (aimRotation.Yaw != 0) AimNode.Aim.X = aimRotation.Yaw / ((aimRotation.Yaw < 0 ? AimNodeMinMaxYaw.X : AimNodeMinMaxYaw.Y) * DegToUnrRot); } if (SpeedNode != None) { SpeedNode.ScaleByValue = FClamp(CurrentSpeed / CatAnimRunSpeed, SpeedNodeMinMaxRate.X, SpeedNodeMinMaxRate.Y); } } // This is a slightly different method than what native code seems to do. // Native code seems to always apply Translation/Rotation in local space. // I am applying transformations in world space for convenience, // then reverting to local space when players are not nearby. function UpdateCollisionMeshes() { local bool shouldUpdate; local vector newLocation; local rotator newRotation; shouldUpdate = (!OptimizeTrainCollisions || IsPlayerNearby); if (CatCollision != None && CatMesh != None && (shouldUpdate || CatCollision.CollideActors)) { newLocation = CatCollision.default.Translation; newRotation = CatCollision.default.Rotation; if (shouldUpdate) { newLocation = CatMesh.Translation; newRotation = CatMesh.Rotation; newLocation += CatCollisionOffset >> newRotation; newRotation += CatCollisionRotationOffset; } CatCollision.SetTranslation(newLocation); CatCollision.SetRotation(newRotation); CatCollision.SetAbsolute(shouldUpdate, shouldUpdate); // This isn't working, but only when going back to relative, and only for *this* component. Why?? CatCollision.SetActorCollision(shouldUpdate, CatCollision.BlockActors, true); // This prevents it from causing any issues at least } if (TrainFrontHit != None && TrainMesh != None && (shouldUpdate || TrainFrontHit.CollideActors)) { newLocation = TrainFrontHit.default.Translation; newRotation = TrainFrontHit.default.Rotation; if (shouldUpdate) { newLocation = Location; newRotation = Rotation; if (TrainMesh.MatchRefBone(TrainFrontHitBoneName) != INDEX_NONE) { newLocation = TrainMesh.GetBoneLocation(TrainFrontHitBoneName); newRotation = QuatToRotator(TrainMesh.GetBoneQuaternion(TrainFrontHitBoneName)); } newLocation += TrainFrontHitOffset >> newRotation; newRotation += TrainFrontHitRotationOffset; } TrainFrontHit.SetTranslation(newLocation); TrainFrontHit.SetRotation(newRotation); TrainFrontHit.SetAbsolute(shouldUpdate, shouldUpdate); TrainFrontHit.SetActorCollision(shouldUpdate, TrainFrontHit.BlockActors, true); } } defaultproperties { // MCU8-ADD: from original Hat_Hazard_CatTrain Begin Object Name=StaticMeshComponent0 StaticMesh=StaticMesh'HatinTime_PrimitiveShapes.TexPropCube' Materials(0)=Material'HatInTime_Levels_ScienceTrain.Materials.Objects.floorpanel_lockon' HiddenGame=true HiddenEditor=true LightEnvironment = None bUsePrecomputedShadows=false End Object Begin Object Class=SkeletalMeshComponent Name=Model0 SkeletalMesh=SkeletalMesh'hatintime_levels_metro_h.models.test_train' PhysicsAsset=PhysicsAsset'hatintime_levels_metro_h.test_train_phys' AnimTreeTemplate=AnimTree'hatintime_levels_metro_h.test_train_tree' CanBlockCamera=true bUsePrecomputedShadows=false HiddenGame=true// during initialization HiddenEditor=true LightEnvironment=MyLightEnvironment Translation=(Z=-23) Rotation=(Roll=16384) MotionBlurInstanceScale=0 bNoSelfShadow=true bCastDynamicShadow=false// looks bad more often than not `if(`isdefined(IS_NINTENDO_SWITCH)) MaxDrawDistance = 6500 `else MaxDrawDistance = 14000 `endif End Object TrainMesh=Model0 Components.Add(Model0) Begin Object Class=SkeletalMeshComponent Name=Model1 SkeletalMesh=SkeletalMesh'HatInTime_Characters_CatMetro.models.catmetro_skm' PhysicsAsset = PhysicsAsset'HatInTime_Characters_CatMetro.Physics.catmetro_skm_Physics' AnimSets(0)=AnimSet'HatInTime_Characters_CatMetro.AnimSets.catmetro_skm_Anims' AnimTreeTemplate=AnimTree'HatInTime_Characters_CatMetro.catmetro_skm_AnimTree' CanBlockCamera=true HiddenGame=true// during initialization bUsePrecomputedShadows=false LightEnvironment=MyLightEnvironment MotionBlurInstanceScale=0 bNoSelfShadow=true bCastDynamicShadow=false// looks bad more often than not `if(`isdefined(IS_NINTENDO_SWITCH)) MinLodModel = 1; MaxDrawDistance = 6500 `else MaxDrawDistance = 14000 `endif End Object CatMesh=Model1 Components.Add(Model1) Begin Object Class=SpotLightComponent Name=SpotLightComponent0 LightAffectsClassification=LAC_DYNAMIC_AND_STATIC_AFFECTING bForceDynamicLight=TRUE LightingChannels=(BSP=TRUE,Static=TRUE,Dynamic=TRUE,CompositeDynamic=TRUE) Brightness=5 LightColor=(R=255,G=240,B=120) Radius=2048 FalloffExponent = 3 InnerConeAngle = 22 OuterConeAngle = 44 Rotation = (Pitch=-1820) CullDistance = 4000 DetailMode = DM_Medium bAffectCompositeShadowDirection = false; bCastCompositeShadow = false; bEnabled = false; bEnabledInEditor = false; End Object Headlight=SpotLightComponent0 Components.Add(SpotLightComponent0) Begin Object Class=StaticMeshComponent Name=CatCollision0 StaticMesh=StaticMesh'HatinTime_PrimitiveShapes.TexPropCube' HiddenGame = true HiddenEditor = true CollideActors = true BlockActors = false Scale3D = (X=1.2,Y=0.7,Z=0.8) AlwaysCheckCollision = true End Object CatCollision = CatCollision0 CollisionComponent = CatCollision0 Components.Add(CatCollision0) Begin Object Class=StaticMeshComponent Name=TrainFrontHit0 StaticMesh=StaticMesh'HatinTime_PrimitiveShapes.TexPropCube' HiddenGame = true HiddenEditor = true CollideActors = true BlockActors = false CanBeEdgeGrabbed = false CanBeWallSlid = false Scale3D = (X=0.1,Y=1.5,Z=1.1) AlwaysCheckCollision = true End Object TrainFrontHit = TrainFrontHit0// (the cow catcher) Components.Add(TrainFrontHit0) Begin Object Class=SpriteComponent Name=SpriteLensFlareEye0 Sprite = Material'HatInTime_Levels_Metro_M.Materials.CatEyesLight' SpriteColor = (R=1,G=0.86,B=0.2,A=0.2) UL = 1000 VL = 1000 BlendMode = BLEND_Translucent MaxDrawDistance = 7000 HiddenGame = true // set in initialization HiddenEditor = true End Object Components.Add(SpriteLensFlareEye0) SpriteEyes(0) = SpriteLensFlareEye0 Begin Object Class=SpriteComponent Name=SpriteLensFlareEye1 Sprite = Material'HatInTime_Levels_Metro_M.Materials.CatEyesLight' SpriteColor = (R=1,G=0.86,B=0.2,A=0.2) UL = 1000 VL = 1000 BlendMode = BLEND_Translucent MaxDrawDistance = 7000 HiddenGame = true // set in initialization HiddenEditor = true End Object Components.Add(SpriteLensFlareEye1) SpriteEyes(1) = SpriteLensFlareEye1 Begin Object Class=SpriteComponent Name=SpriteLensflareHeadlight0 Sprite = Material'HatInTime_Levels_Metro_M.Materials.CatEyesLight' SpriteColor = (R=0.000023,G=1.0,B=0.625345,A=0.5) UL = 1000 VL = 1000 BlendMode = BLEND_Translucent MaxDrawDistance = 7000 HiddenGame = true // set in initialization HiddenEditor = true End Object Components.Add(SpriteLensflareHeadlight0); SpriteHeadlights(0) = SpriteLensflareHeadlight0 Begin Object Class=SpriteComponent Name=SpriteLensflareHeadlight1 Sprite = Material'HatInTime_Levels_Metro_M.Materials.CatEyesLight' SpriteColor = (R=0.000023,G=1.0,B=0.625345,A=0.5) UL = 1000 VL = 1000 BlendMode = BLEND_Translucent MaxDrawDistance = 7000 HiddenGame = true // set in initialization HiddenEditor = true End Object Components.Add(SpriteLensflareHeadlight1); SpriteHeadlights(1) = SpriteLensflareHeadlight1 Begin Object Class=AudioComponent Name=AmbientSoundComponent0 SoundCue=SoundCue'HatinTime_SFX_Metro.SoundCues.CatTrains_Purring_Loop' bAutoPlay=false bStopWhenOwnerDestroyed=true End Object TrainFrontSound=AmbientSoundComponent0 Components.Add(AmbientSoundComponent0) Begin Object Class=AudioComponent Name=AmbientSoundComponent1 SoundCue=SoundCue'HatinTime_SFX_Metro.SoundCues.CatTrains_Movement_Driving_Loop_Standart' bAutoPlay=false bStopWhenOwnerDestroyed=true End Object TrainWheelSound=AmbientSoundComponent1 Components.Add(AmbientSoundComponent1) Speed = 900 CatAnimRunSpeed = 880 OrientPlayerCamera = true UseShakeCamera = true InstantTeleport = true IgnoreFrontDamageWhileAirborne = true UseFastTrainVFX = true IsDamageFront = true DamageOnAnyTouch = true OptimizeTrainCollisions = true WarningCameraShakeLead = 100 CameraShakeFalloff = 900 UseTrainHorn = true TrainHornLead = 500 TrainHornSize = 200 FrontClip = 2.48 RearClip = 0.76 bBlockActors = false CarCollisionSize = (X=320,Y=320,Z=360) CarsInTrain = 16 CarCollisionDensity = 2 BoneDensity = 320 AnimTreeControls = ("wagon1","wagon2","wagon3","wagon4","wagon5","wagon6","wagon7","wagon8","wagon9","wagon10","wagon11","wagon12","wagon13","wagon14","wagon15","wagon16","wagon_end") AverageCorners = 0.5 TrainHornSound = SoundCue'HatinTime_SFX_Metro.SoundCues.metrotrain_warninghorn' FootStepSound = SoundCue'HatinTime_SFX_Metro.SoundCues.CatTrains_Movement_Paw_Footstep' HoloStepSound = SoundCue'HatinTime_SFX_Metro.SoundCues.holorail_footstep' MeowVoiceClip = SoundCue'HatinTime_SFX_Metro.SoundCues.CatTrains_Vocal' VoiceInterval = (X=15,Y=40) EditorConvertMenuName = "Length" EditorConvertActorList.Add((Text="Long", ClassName="sb64_Hazard_CatTrain")) EditorConvertActorList.Add((Text="Short", ClassName="sb64_Hazard_CatTrain_Short")) EditorConvertActorList.Add((Text="Dweller", ClassName="sb64_Hazard_CatTrain_Dweller")) // MCU8-ADD-END SpeedNodeMinMaxRate = (X=0.5,Y=2.0) AimNodeTrainBoneNameA = "wagon2" AimNodeTrainBoneNameB = "wagon3" AimNodeMinMaxPitch = (X=-90.0,Y=90.0) AimNodeMinMaxYaw = (X=-90.0,Y=90.0) CatMeshTrainBoneName = "wagon2" CatMeshOffset = (X=10.0,Z=-165.0) TrainMeshOffset = (Z=-24.0) CatCollisionOffset = (Z=90.0) CatCollisionRotationOffset = (Pitch=0,Yaw=0,Roll=0) TrainFrontHitBoneName = "wagon3" TrainFrontHitOffset = (X=100.0) TrainFrontHitRotationOffset = (Pitch=-5461,Yaw=0,Roll=16384) DebugRenderCollisionBoxes = false; }