rimworld-animations/1.6/Source/Comps/CompExtendedAnimator.cs
2025-09-12 22:05:20 -07:00

468 lines
16 KiB
C#

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<AnimationDef> 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<Pawn> 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<Pawn> actors)
{
this.anchor = anchor;
PlayGroupAnimation(groupAnimationDef, animationIndex, randomSeed, actors);
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look<bool>(ref this.isAnimating, "animations_isAnimating", false);
Scribe_Values.Look<int>(ref this.animationTicks, "animations_ticks", 0);
Scribe_Values.Look<int>(ref this.actorIndex, "actor_index", 0);
Scribe_Collections.Look<AnimationDef>(ref animationQueue, "animations_queue");
Scribe_Defs.Look<GroupAnimationDef>(ref currentGroupAnimation, "animations_groupAnimationDef");
Scribe_Deep.Look<BaseExtendedAnimatorAnchor>(ref this.anchor, "animations_anchor");
Scribe_Values.Look<Vector3>(ref this.offset, "animations_offset", Vector3.zero);
Scribe_Values.Look<int>(ref this.rotation, "animations_rotationOffset", 0);
Scribe_Defs.Look<VoiceDef>(ref this.voice, "animations_voice");
}
public override List<PawnRenderNode> CompRenderNodes()
{
//only if pawn is animating for performance
if (IsAnimating)
{
List<PawnRenderNode> animRenderNodes = new List<PawnRenderNode>();
// for all animationpropdefs,
foreach (AnimationPropDef animationProp in DefDatabase<AnimationPropDef>.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<VoiceDef> voiceOptions =
DefDatabase<VoiceDef>.AllDefsListForReading
.FindAll(voiceDef => voiceDef.VoiceFitsPawn(pawn));
//all voice options, with priority (for traitdef specific voices)
List<VoiceDef> 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;
}
}