using System; using System.Collections.Generic; using System.Linq; using RimWorld; using rjw; using UnityEngine; using Verse; using Verse.AI; using Verse.Sound; namespace Rimworld_Animations { public class CompExtendedAnimator : ThingComp { // CompExtendedAnimator // Helps manage AnimationQueue, AbsolutePosition //ticks of current animation private int animationTicks, actorIndex; private int ticksSinceLastVoicePlayed, ticksUntilNextVoice; private GroupAnimationDef currentGroupAnimation; private List animationQueue; private BaseExtendedAnimatorAnchor anchor; private VoiceDef voice; private VoiceTagDef lastVoiceTag; private bool isAnimating = false; public bool IsAnimating { get { return isAnimating; } } public bool IsAnchored { get { return anchor != null; } } private Vector3 offset; public Vector3? Offset { get { return offset; } set { this.offset = value ?? Vector3.zero; } } private int rotation; public int? Rotation { get { return rotation; } set { this.rotation = value ?? 0; } } public int AnimationLength { get { if (!IsAnimating) return 0; int groupAnimLength = 0; foreach(AnimationDef anim in animationQueue) { groupAnimLength += anim.durationTicks; } return groupAnimLength; } } public AnimationDef CurrentAnimation { get { return IsAnimating ? animationQueue[0] : null; } } public GroupAnimationDef CurrentGroupAnimation { get { return currentGroupAnimation; } set { currentGroupAnimation = value; } } public int ActorIndex { get { return actorIndex; } } public override void PostSpawnSetup(bool respawningAfterLoad) { if (voice == null) { AssignNewVoice(); } } public Vector3 getAnchor() { return anchor.getDrawPos(); } public override void CompTick() { if (isAnimating) { animationTicks++; //if animationticks is equal to cur. anim duration, if (animationTicks >= animationQueue[0].durationTicks) { //dequeue; returns false if more animations if (!PopAnimationQueue()) { //play next if more anims still PlayNextAnimation(); } else { StopAnimating(); } } CheckAndPlaySounds(); } base.CompTick(); } //returns false if still more animations public bool PopAnimationQueue() { if (!animationQueue.Empty()) { //pop queue animationQueue.RemoveAt(0); } return animationQueue.Empty(); } public void PlayNextAnimation() { if (!animationQueue.Empty()) { isAnimating = true; animationTicks = 0; pawn.Drawer.renderer.SetAnimation(animationQueue[0]); } } public void StopAnimating() { isAnimating = false; animationQueue = null; anchor = null; offset = Vector3.zero; pawn.Drawer.renderer.SetAnimation(null); CurrentGroupAnimation = null; actorIndex = 0; pawn.Drawer.renderer.SetAllGraphicsDirty(); } public void PlayGroupAnimation(GroupAnimationDef groupAnimationDef, int actorIndex, int randomSeed, List actors) { this.actorIndex = actorIndex; this.currentGroupAnimation = groupAnimationDef; ticksSinceLastVoicePlayed = GenTicks.TicksGame; ticksUntilNextVoice = 0; groupAnimationDef.GetOffset(actorIndex, this.pawn, out Vector3? offset, out int? rotation); this.Offset = offset; this.Rotation = rotation; animationQueue = groupAnimationDef.GetAllAnimationsForActor(actorIndex, randomSeed, actors); //set all graphics dirty; necessary because sometimes rjw doesn't call during threesomes pawn.Drawer.renderer.SetAllGraphicsDirty(); PlayNextAnimation(); } public void PlayGroupAnimation(GroupAnimationDef groupAnimationDef, BaseExtendedAnimatorAnchor anchor, int animationIndex, int randomSeed, List actors) { this.anchor = anchor; PlayGroupAnimation(groupAnimationDef, animationIndex, randomSeed, actors); } public override void PostExposeData() { base.PostExposeData(); Scribe_Values.Look(ref this.isAnimating, "animations_isAnimating", false); Scribe_Values.Look(ref this.animationTicks, "animations_ticks", 0); Scribe_Values.Look(ref this.actorIndex, "actor_index", 0); Scribe_Collections.Look(ref animationQueue, "animations_queue"); Scribe_Defs.Look(ref currentGroupAnimation, "animations_groupAnimationDef"); Scribe_Deep.Look(ref this.anchor, "animations_anchor"); Scribe_Values.Look(ref this.offset, "animations_offset", Vector3.zero); Scribe_Values.Look(ref this.rotation, "animations_rotationOffset", 0); Scribe_Defs.Look(ref this.voice, "animations_voice"); } public override List CompRenderNodes() { //only if pawn is animating for performance if (IsAnimating) { List animRenderNodes = new List(); // for all animationpropdefs, foreach (AnimationPropDef animationProp in DefDatabase.AllDefsListForReading) { //if animation makes use of prop, if (AnimationMakesUseOfProp(animationProp, out PawnRenderNodeProperties props)) { if (props.texPath.NullOrEmpty()) { props.texPath = "AnimationProps/MissingTexture/MissingTexture"; } //create new render node PawnRenderNode animRenderNode = (PawnRenderNode)Activator.CreateInstance(props.nodeClass, new object[] { this.pawn, props, pawn.Drawer.renderer.renderTree }); animRenderNodes.Add(animRenderNode); } } //return list of rendernodes that should animate return animRenderNodes; } else { return null; } } public void AssignNewVoice() { //all voice options List voiceOptions = DefDatabase.AllDefsListForReading .FindAll(voiceDef => voiceDef.VoiceFitsPawn(pawn)); //all voice options, with priority (for traitdef specific voices) List voiceOptionsWithPriority = voiceOptions.FindAll(voiceDef => voiceDef.takesPriority); if (!voiceOptionsWithPriority.NullOrEmpty()) { voice = voiceOptionsWithPriority.RandomElementByWeight(x => x.randomChanceFactor); } else if (!voiceOptions.NullOrEmpty()) { voice = voiceOptions.RandomElementByWeight(x => x.randomChanceFactor); } } public void CheckAndPlaySounds() { PawnRenderNode rootNode = pawn.Drawer?.renderer?.renderTree?.rootNode; //check if the rootnode has sounds; if so play it if (rootNode?.AnimationWorker is AnimationWorker_KeyframesExtended animWorker) { SoundDef sound = animWorker.soundAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode); if (sound != null) { SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map); //temp; does not consider non-rjw animations //todo: replace with value stored in comp or somewhere else? soundInfo.volumeFactor = RJWAnimationSettings.soundVolume; sound.PlayOneShot(soundInfo); } if (RJWAnimationSettings.playVoices) { SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map); voiceInfo.volumeFactor = RJWAnimationSettings.voicesVolume; //play voice sounds VoiceTagDef voiceTag = animWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode); if (voiceTag != null && (lastVoiceTag != voiceTag || ticksSinceLastVoicePlayed + ticksUntilNextVoice < GenTicks.TicksGame)) //play a new voice tag, or wait until time elapsed { lastVoiceTag = voiceTag; ticksSinceLastVoicePlayed = GenTicks.TicksGame; if (voice != null && voice.sounds.ContainsKey(voiceTag)) { ticksUntilNextVoice = voice.ticksBetweenPlays.RandomInRange; voice.sounds[voiceTag].PlayOneShot(voiceInfo); } else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault) { //play default voice VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale); ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange; if (pawnDefaultVoice.sounds.ContainsKey(voiceTag)) { ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange; pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo); } } } } } //check rootnodes and children if (rootNode?.children != null) { foreach (PawnRenderNode node in rootNode?.children) { if (node?.AnimationWorker is AnimationWorker_KeyframesExtended childrenAnimWorker) { if (RJWAnimationSettings.playSounds) { SoundDef sound = childrenAnimWorker.soundAtTick(node.tree.AnimationTick, node.tree.currentAnimation, node); if (sound != null) { SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map); soundInfo.volumeFactor = RJWAnimationSettings.soundVolume; sound.PlayOneShot(soundInfo); } } if (RJWAnimationSettings.playVoices) { SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map); voiceInfo.volumeFactor = RJWAnimationSettings.voicesVolume; //play voice sounds VoiceTagDef voiceTag = childrenAnimWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode); if (voiceTag != null && (lastVoiceTag != voiceTag || ticksSinceLastVoicePlayed + ticksUntilNextVoice < GenTicks.TicksGame)) //play a new voice tag, or wait until time elapsed { lastVoiceTag = voiceTag; ticksSinceLastVoicePlayed = GenTicks.TicksGame; if (voice != null && voice.sounds.ContainsKey(voiceTag)) { ticksUntilNextVoice = voice.ticksBetweenPlays.RandomInRange; voice.sounds[voiceTag].PlayOneShot(voiceInfo); } else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault) { VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale); if (pawnDefaultVoice.sounds.ContainsKey(voiceTag)) { ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange; pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo); } } } } } } } //do the same for all the child nodes } public bool AnimationMakesUseOfProp(AnimationPropDef animationProp, out PawnRenderNodeProperties props) { props = null; // never true if not animating; anim props shouldn't be attached if (!IsAnimating) return false; //for all anims in queue (because it's only recached at start) foreach (AnimationDef animation in animationQueue) { foreach (PawnRenderNodeTagDef propTag in animation.keyframeParts.Keys) { //get specific props for that pawn props = animationProp.GetPawnRenderNodeProperties(this.pawn); // if that proptag is the same as the one for animationProp, if (propTag == props.tagDef) { //that prop is being used in the animation return true; } } } return false; } private Pawn pawn => base.parent as Pawn; } }