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; private List animationQueue; private BaseExtendedAnimatorAnchor anchor; private VoiceDef voice; 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; } } private int? rotation; public int? Rotation { get { return rotation; } set { this.rotation = value; } } 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 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 = null; pawn.Drawer.renderer.SetAnimation(null); pawn.Drawer.renderer.SetAllGraphicsDirty(); } public void PlayGroupAnimation(List groupAnimation, Vector3? positionOffset, int? rotationOffset) { this.Offset = positionOffset; this.Rotation = rotationOffset; animationQueue = groupAnimation; //set all graphics dirty; necessary because sometimes rjw doesn't call during threesomes pawn.Drawer.renderer.SetAllGraphicsDirty(); PlayNextAnimation(); } public void PlayGroupAnimation(List groupAnimation, Vector3? positionOffset, int? rotationOffset, BaseExtendedAnimatorAnchor anchor) { this.anchor = anchor; PlayGroupAnimation(groupAnimation, positionOffset, rotationOffset); } 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_Collections.Look(ref animationQueue, "animations_queue"); Scribe_Deep.Look(ref this.anchor, "animations_anchor"); 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)) { PawnRenderNodeProperties props = animationProp.animPropProperties; 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 = RJWSettings.sounds_sex_volume; sound.PlayOneShot(soundInfo); } if (RJWAnimationSettings.playVoices) { SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map); voiceInfo.volumeFactor = RJWSettings.sounds_voice_volume; //play voice sounds VoiceTagDef voiceTag = animWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode); if (voiceTag != null) { if (voice != null && voice.sounds.ContainsKey(voiceTag)) { 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); if (pawnDefaultVoice.sounds.ContainsKey(voiceTag)) { 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) { SoundDef sound = childrenAnimWorker.soundAtTick(node.tree.AnimationTick, node.tree.currentAnimation, node); if (sound != null) { SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map); soundInfo.volumeFactor = RJWSettings.sounds_sex_volume; sound.PlayOneShot(soundInfo); } if (RJWAnimationSettings.playVoices) { SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map); voiceInfo.volumeFactor = RJWSettings.sounds_voice_volume; //play voice sounds VoiceTagDef voiceTag = childrenAnimWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode); if (voiceTag != null) { if (voice != null && voice.sounds.ContainsKey(voiceTag)) { 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)) { pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo); } } } } } } } //do the same for all the child nodes } public bool AnimationMakesUseOfProp(AnimationPropDef animationProp) { // 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) { // if that proptag is the same as the one for animationProp, if (propTag == animationProp.animPropProperties.tagDef) { //that prop is being used in the animation return true; } } } return false; } private Pawn pawn => base.parent as Pawn; } }