Client LuaCsForBarotrauma
CharacterEditorScreen.cs
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Input;
3 using Microsoft.Xna.Framework.Graphics;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
9 using Barotrauma.Sounds;
10 using FarseerPhysics;
11 using FarseerPhysics.Dynamics;
12 #if DEBUG
13 using System.IO;
14 #else
15 using Barotrauma.IO;
16 #endif
17 
19 {
21  {
22  public static CharacterEditorScreen Instance { get; private set; }
23 
24  private Camera cam;
25  public override Camera Cam
26  {
27  get
28  {
29  cam ??= new Camera()
30  {
31  MinZoom = 0.1f,
32  MaxZoom = 5.0f
33  };
34  return cam;
35  }
36  }
37 
38  private bool ShowExtraRagdollControls => editLimbs || editJoints;
39 
40  public Character SpawnedCharacter => character;
41  private Character character;
42  private Vector2 spawnPosition;
43 
44  private bool editCharacterInfo;
45  private bool editRagdoll;
46  private bool editAnimations;
47  private bool editLimbs;
48  private bool editJoints;
49  private bool editIK;
50 
51  private bool drawSkeleton;
52  private bool drawDamageModifiers;
53  private bool showParamsEditor;
54  private bool showSpritesheet;
55  private bool isFrozen;
56  private bool autoFreeze;
57  private bool limbPairEditing;
58  private bool uniformScaling;
59  private bool lockSpriteOrigin;
60  private bool lockSpritePosition;
61  private bool lockSpriteSize;
62  private bool recalculateCollider;
63  private bool copyJointSettings;
64  private bool showColliders;
65  private bool displayWearables;
66  private bool displayBackgroundColor;
67  private bool onlyShowSourceRectForSelectedLimbs;
68  private bool unrestrictSpritesheet;
69 
70  private enum JointCreationMode
71  {
72  None,
73  Select,
74  Create
75  }
76 
77  private JointCreationMode jointCreationMode;
78  private bool isDrawingLimb;
79 
80  private Rectangle newLimbRect;
81  private Limb jointStartLimb;
82  private Limb jointEndLimb;
83  private Vector2? anchor1Pos;
84 
85  private const float holdTime = 0.2f;
86  private double holdTimer;
87 
88  private float spriteSheetZoom = 1;
89  private float spriteSheetMinZoom = 0.25f;
90  private float spriteSheetMaxZoom = 1;
91  private const int spriteSheetOffsetY = 20;
92  private const int spriteSheetOffsetX = 30;
93  private bool hideBodySheet;
94  private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f);
95  private Vector2 cameraOffset;
96 
97  private readonly List<LimbJoint> selectedJoints = new List<LimbJoint>();
98  private readonly List<Limb> selectedLimbs = new List<Limb>();
99  private readonly HashSet<Character> editedCharacters = new HashSet<Character>();
100 
101  private bool isEndlessRunner;
102 
103  private Rectangle spriteSheetRect;
104 
105  private Rectangle CalculateSpritesheetRectangle() =>
106  Textures == null || Textures.None() ? Rectangle.Empty :
107  new Rectangle(
108  spriteSheetOffsetX,
109  spriteSheetOffsetY,
110  (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom),
111  (int)(Textures.Sum(t => t.Height) * spriteSheetZoom));
112 
113  private const string screenTextTag = "CharacterEditor.";
114 
115  public override void Select()
116  {
117  base.Select();
118 
120 
121  GUI.ForceMouseOn(null);
122  if (Submarine.MainSub == null)
123  {
124  ResetVariables();
125  var subInfo = new SubmarineInfo("Content/AnimEditor.sub");
126  Submarine.MainSub = new Submarine(subInfo, showErrorMessages: false);
127  if (Submarine.MainSub.PhysicsBody != null)
128  {
130  }
131  wallGroups[0] = new WallGroup(new List<MapEntity>(MapEntity.MapEntityList));
132  CloneWalls();
133  CalculateMovementLimits();
134  isEndlessRunner = true;
135  GameMain.LightManager.LightingEnabled = false;
136  }
137  else if (Instance == null)
138  {
139  ResetVariables();
140  }
141  Submarine.MainSub.GodMode = true;
142  if (Character.Controlled == null)
143  {
144  var humanSpeciesName = CharacterPrefab.HumanSpeciesName;
145  if (humanSpeciesName.IsEmpty)
146  {
147  SpawnCharacter(VisibleSpecies.First());
148  }
149  else
150  {
151  SpawnCharacter(humanSpeciesName);
152  }
153  }
154  else
155  {
156  OnPreSpawn();
157  character = Character.Controlled;
158  OnPostSpawn();
159  }
160  OpenDoors();
161  GameMain.Instance.ResolutionChanged += OnResolutionChanged;
162  Instance = this;
163  }
164 
165  private void ResetVariables()
166  {
167  editCharacterInfo = false;
168  editRagdoll = false;
169  editAnimations = false;
170  editLimbs = false;
171  editJoints = false;
172  editIK = false;
173  drawSkeleton = false;
174  drawDamageModifiers = false;
175  showParamsEditor = false;
176  showSpritesheet = false;
177  isFrozen = false;
178  autoFreeze = false;
179  limbPairEditing = false;
180  uniformScaling = true;
181  lockSpriteOrigin = true;
182  lockSpritePosition = false;
183  lockSpriteSize = false;
184  recalculateCollider = false;
185  copyJointSettings = false;
186  showColliders = false;
187  displayWearables = true;
188  displayBackgroundColor = false;
189  jointCreationMode = JointCreationMode.None;
190  isDrawingLimb = false;
191  newLimbRect = Rectangle.Empty;
192  cameraOffset = Vector2.Zero;
193  jointEndLimb = null;
194  anchor1Pos = null;
195  jointStartLimb = null;
196  visibleSpecies = null;
197  onlyShowSourceRectForSelectedLimbs = false;
198  unrestrictSpritesheet = false;
199  editedCharacters.Clear();
200  selectedJoints.Clear();
201  selectedLimbs.Clear();
202  if (character != null)
203  {
204  if (character.AnimController != null)
205  {
206  if (character.AnimController.Collider != null)
207  {
208  character.AnimController.Collider.PhysEnabled = true;
209  }
210  }
211  }
212  character = null;
213  Wizard.instance?.Reset();
214  }
215 
216  private void Reset(IEnumerable<Character> characters = null)
217  {
218  characters ??= editedCharacters;
219  characters.ForEach(c => ResetParams(c));
220  ResetVariables();
221  }
222 
223  private static void ResetParams(Character character)
224  {
225  character.Params.Reset(true);
226  foreach (var animation in character.AnimController.AllAnimParams)
227  {
228  animation.Reset(true);
229  animation.ClearHistory();
230  }
231  character.AnimController.RagdollParams.Reset(true);
232  character.AnimController.RagdollParams.ClearHistory();
233  character.ForceRun = false;
234  character.AnimController.ForceSelectAnimationType = AnimationType.NotDefined;
235  }
236 
237  protected override void DeselectEditorSpecific()
238  {
239  SoundPlayer.OverrideMusicType = Identifier.Empty;
240  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, GameSettings.CurrentConfig.Audio.SoundVolume, 0);
241  GUI.ForceMouseOn(null);
242  if (isEndlessRunner)
243  {
245  GameMain.World.ProcessChanges();
246  isEndlessRunner = false;
247  Reset();
248  if (character != null && !character.Removed)
249  {
250  character.Remove();
251  }
252  }
253  else
254  {
255 #if !DEBUG
256  Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.Prefab.ContentFile)));
257 #endif
258  }
259  GameMain.Instance.ResolutionChanged -= OnResolutionChanged;
260  if (!GameMain.DevMode)
261  {
262  GameMain.LightManager.LightingEnabled = true;
263  }
264  ClearWidgets();
265  ClearSelection();
266  }
267 
268  private void OnResolutionChanged()
269  {
270  CreateGUI();
271  }
272 
274  {
275  return TextManager.Get(screenTextTag + tag);
276  }
277 
278 #region Main methods
279  public override void AddToGUIUpdateList()
280  {
281  if (rightArea == null || leftArea == null) { return; }
282  rightArea.AddToGUIUpdateList();
283  leftArea.AddToGUIUpdateList();
284 
286  if (displayBackgroundColor)
287  {
288  backgroundColorPanel.AddToGUIUpdateList();
289  }
290  if (editAnimations)
291  {
292  animationControls.AddToGUIUpdateList();
293  }
294  if (showSpritesheet)
295  {
296  spriteSheetControls.AddToGUIUpdateList();
297  Limb lastLimb = selectedLimbs.LastOrDefault();
298  if (lastLimb == null)
299  {
300  var lastJoint = selectedJoints.LastOrDefault();
301  if (lastJoint != null)
302  {
303  lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA;
304  }
305  }
306  if (lastLimb != null)
307  {
308  resetSpriteOrientationButtonParent.AddToGUIUpdateList();
309  }
310  }
311  if (editRagdoll)
312  {
313  ragdollControls.AddToGUIUpdateList();
314  }
315  if (editJoints)
316  {
317  jointControls.AddToGUIUpdateList();
318  }
319  if (editLimbs && !unrestrictSpritesheet)
320  {
321  limbControls.AddToGUIUpdateList();
322  }
323  if (ShowExtraRagdollControls)
324  {
325  createLimbButton.Enabled = editLimbs;
326  duplicateLimbButton.Enabled = selectedLimbs.Any();
327  deleteSelectedButton.Enabled = selectedLimbs.Any() || selectedJoints.Any();
328  createJointButton.Enabled = selectedLimbs.Any() || selectedJoints.Any();
329  extraRagdollControls.AddToGUIUpdateList();
330  if (createLimbButton.Enabled)
331  {
332  if (isDrawingLimb)
333  {
334  createLimbButton.Color = Color.Yellow;
335  createLimbButton.HoverColor = Color.Yellow;
336  }
337  else
338  {
339  createLimbButton.Color = Color.White;
340  createLimbButton.HoverColor = Color.White;
341  }
342  }
343  if (createJointButton.Enabled)
344  {
345  switch (jointCreationMode)
346  {
347  case JointCreationMode.Select:
348  case JointCreationMode.Create:
349  createJointButton.HoverColor = Color.Yellow;
350  createJointButton.Color = Color.Yellow;
351  break;
352  default:
353  createJointButton.HoverColor = Color.White;
354  createJointButton.Color = Color.White;
355  break;
356  }
357  }
358  }
359  if (showParamsEditor)
360  {
362  }
363  }
364 
365  public override void Update(double deltaTime)
366  {
367  base.Update(deltaTime);
368  if (Wizard.instance != null) { return; }
369 
370  GameMain.LightManager?.Update((float)deltaTime);
371 
372  spriteSheetRect = CalculateSpritesheetRectangle();
373  // Handle shortcut keys
374  if (PlayerInput.KeyHit(Keys.F1))
375  {
376  SetToggle(paramsToggle, !paramsToggle.Selected);
377  }
378  if (PlayerInput.KeyHit(Keys.F5))
379  {
380  RecreateRagdoll();
381  }
382  if (GUI.KeyboardDispatcher.Subscriber == null)
383  {
384  if (PlayerInput.KeyHit(Keys.D1))
385  {
386  SetToggle(characterInfoToggle, !characterInfoToggle.Selected);
387  }
388  else if (PlayerInput.KeyHit(Keys.D2))
389  {
390  SetToggle(ragdollToggle, !ragdollToggle.Selected);
391  }
392  else if (PlayerInput.KeyHit(Keys.D3))
393  {
394  SetToggle(limbsToggle, !limbsToggle.Selected);
395  }
396  else if (PlayerInput.KeyHit(Keys.D4))
397  {
398  SetToggle(jointsToggle, !jointsToggle.Selected);
399  }
400  else if (PlayerInput.KeyHit(Keys.D5))
401  {
402  SetToggle(animsToggle, !animsToggle.Selected);
403  }
404  if (PlayerInput.KeyDown(Keys.LeftControl))
405  {
406  Character.DisableControls = true;
407  Widget.EnableMultiSelect = !editAnimations;
408  // Undo/Redo
409  if (PlayerInput.KeyHit(Keys.Z))
410  {
411  if (editJoints || editLimbs || editIK)
412  {
414  character.AnimController.ResetJoints();
415  character.AnimController.ResetLimbs();
416  ClearWidgets();
417  CreateGUI();
418  ResetParamsEditor();
419  }
420  if (editAnimations)
421  {
422  CurrentAnimation.Undo();
423  ClearWidgets();
424  ResetParamsEditor();
425  }
426  }
427  else if (PlayerInput.KeyHit(Keys.R))
428  {
429  if (editJoints || editLimbs || editIK)
430  {
432  character.AnimController.ResetJoints();
433  character.AnimController.ResetLimbs();
434  ClearWidgets();
435  CreateGUI();
436  ResetParamsEditor();
437  }
438  if (editAnimations)
439  {
440  CurrentAnimation.Redo();
441  ClearWidgets();
442  ResetParamsEditor();
443  }
444  }
445  }
446  else
447  {
448  Widget.EnableMultiSelect = false;
449  if (PlayerInput.KeyHit(Keys.C))
450  {
451  SetToggle(showCollidersToggle, !showCollidersToggle.Selected);
452  }
453  if (PlayerInput.KeyHit(Keys.L))
454  {
455  SetToggle(lightsToggle, !lightsToggle.Selected);
456  }
457  if (PlayerInput.KeyHit(Keys.M))
458  {
459  SetToggle(damageModifiersToggle, !damageModifiersToggle.Selected);
460  }
461  if (PlayerInput.KeyHit(Keys.N))
462  {
463  SetToggle(skeletonToggle, !skeletonToggle.Selected);
464  }
465  if (PlayerInput.KeyHit(Keys.T))
466  {
467  SetToggle(spritesheetToggle, !spritesheetToggle.Selected);
468  }
469  if (PlayerInput.KeyHit(Keys.I))
470  {
471  SetToggle(ikToggle, !ikToggle.Selected);
472  }
473  }
475  {
476  // Enable the main collider physics when the user is trying to move the character.
477  // It's possible that the physics are disabled, because the angle widgets handle input logic in the draw method (which they shouldn't)
478  character.AnimController.Collider.PhysEnabled = true;
479  }
480  animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation;
481  if (animTestPoseToggle.Enabled)
482  {
483  if (PlayerInput.KeyHit(Keys.X))
484  {
485  SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected);
486  }
487  }
488  else
489  {
490  animTestPoseToggle.Selected = false;
491  }
492  if (PlayerInput.KeyHit(InputType.Run))
493  {
494  int index = 0;
495  bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow;
496  bool isMovingFast = character.AnimController.ForceSelectAnimationType == AnimationType.Run || character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast;
497  if (character.AnimController.CanWalk)
498  {
499  if (isMovingFast)
500  {
501  if (isSwimming)
502  {
503  index = 2;
504  }
505  else
506  {
507  index = 0;
508  }
509  }
510  else
511  {
512  if (isSwimming)
513  {
514  index = 3;
515  }
516  else
517  {
518  index = 1;
519  }
520  }
521  }
522  else
523  {
524  index = isMovingFast ? 0 : 1;
525  }
526  if (animSelection.SelectedIndex != index)
527  {
528  CurrentAnimation.ClearHistory();
529  animSelection.Select(index);
530  CurrentAnimation.StoreSnapshot();
531  }
532  }
533  if (!PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E))
534  {
535  bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow;
536  if (isSwimming)
537  {
538  animSelection.Select(0);
539  }
540  else
541  {
542  animSelection.Select(2);
543  }
544  }
546  {
547  bool reset = false;
548  if (selectedLimbs.Any())
549  {
550  selectedLimbs.Clear();
551  reset = true;
552  }
553  if (selectedJoints.Any())
554  {
555  selectedJoints.Clear();
556  foreach (var w in jointSelectionWidgets.Values)
557  {
558  w.Refresh();
559  w.LinkedWidget?.Refresh();
560  }
561  reset = true;
562  }
563  if (reset)
564  {
565  ResetParamsEditor();
566  }
567  jointCreationMode = JointCreationMode.None;
568  isDrawingLimb = false;
569  }
570  if (PlayerInput.KeyHit(Keys.Delete))
571  {
572  DeleteSelected();
573  }
574  if (ShowExtraRagdollControls && PlayerInput.KeyDown(Keys.LeftControl))
575  {
576  if (PlayerInput.KeyHit(Keys.E))
577  {
578  ToggleJointCreationMode();
579  }
580  }
581  UpdateJointCreation();
582  UpdateLimbCreation();
583  if (PlayerInput.KeyHit(Keys.Left))
584  {
585  Nudge(Keys.Left);
586  }
587  if (PlayerInput.KeyHit(Keys.Right))
588  {
589  Nudge(Keys.Right);
590  }
591  if (PlayerInput.KeyHit(Keys.Down))
592  {
593  Nudge(Keys.Down);
594  }
595  if (PlayerInput.KeyHit(Keys.Up))
596  {
597  Nudge(Keys.Up);
598  }
599  if (PlayerInput.KeyDown(Keys.Left))
600  {
601  holdTimer += deltaTime;
602  if (holdTimer > holdTime)
603  {
604  Nudge(Keys.Left);
605  }
606  }
607  else if (PlayerInput.KeyDown(Keys.Right))
608  {
609  holdTimer += deltaTime;
610  if (holdTimer > holdTime)
611  {
612  Nudge(Keys.Right);
613  }
614  }
615  else if (PlayerInput.KeyDown(Keys.Down))
616  {
617  holdTimer += deltaTime;
618  if (holdTimer > holdTime)
619  {
620  Nudge(Keys.Down);
621  }
622  }
623  else if (PlayerInput.KeyDown(Keys.Up))
624  {
625  holdTimer += deltaTime;
626  if (holdTimer > holdTime)
627  {
628  Nudge(Keys.Up);
629  }
630  }
631  else
632  {
633  holdTimer = 0;
634  }
635  if (isFrozen)
636  {
637  float moveSpeed = (float)deltaTime * 300.0f / Cam.Zoom;
638  if (PlayerInput.KeyDown(Keys.LeftShift))
639  {
640  moveSpeed *= 4;
641  }
642  if (PlayerInput.KeyDown(Keys.W))
643  {
644  cameraOffset.Y += moveSpeed;
645  }
646  if (PlayerInput.KeyDown(Keys.A))
647  {
648  cameraOffset.X -= moveSpeed;
649  }
650  if (PlayerInput.KeyDown(Keys.S))
651  {
652  cameraOffset.Y -= moveSpeed;
653  }
654  if (PlayerInput.KeyDown(Keys.D))
655  {
656  cameraOffset.X += moveSpeed;
657  }
658  Vector2 max = new Vector2(GameMain.GraphicsWidth * 0.3f, GameMain.GraphicsHeight * 0.38f) / Cam.Zoom;
659  Vector2 min = -max;
660  cameraOffset = Vector2.Clamp(cameraOffset, min, max);
661  }
662  }
663  if (!isFrozen)
664  {
665  foreach (PhysicsBody body in PhysicsBody.List)
666  {
667  body.SetPrevTransform(body.SimPosition, body.Rotation);
668  body.Update();
669  }
670  // Handle ragdolling here, because we are not calling the Character.Update() method.
672  {
673  character.IsRagdolled = PlayerInput.KeyDown(InputType.Ragdoll);
674  }
675  if (character.IsRagdolled)
676  {
677  character.AnimController.ResetPullJoints();
678  }
679  character.ControlLocalPlayer((float)deltaTime, Cam, false);
680  character.Control((float)deltaTime, Cam);
681  character.AnimController.UpdateAnimations((float)deltaTime);
682  character.AnimController.UpdateRagdoll((float)deltaTime, Cam);
683  character.CurrentHull = character.AnimController.CurrentHull;
684  if (isEndlessRunner)
685  {
686  if (character.Position.X < min)
687  {
688  UpdateWalls(false);
689  }
690  else if (character.Position.X > max)
691  {
692  UpdateWalls(true);
693  }
694  }
695  try
696  {
697  GameMain.World.Step((float)Timing.Step);
698  }
699  catch (WorldLockedException e)
700  {
701  string errorMsg = "Attempted to modify the state of the physics simulation while a time step was running.";
702  DebugConsole.ThrowError(errorMsg, e);
703  GameAnalyticsManager.AddErrorEventOnce("CharacterEditorScreen.Update:WorldLockedException" + e.Message, GameAnalyticsManager.ErrorSeverity.Critical, errorMsg);
704  }
705  }
706  // Camera
707  Cam.MoveCamera((float)deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null);
708  Vector2 targetPos = character.WorldPosition;
710  {
711  // Pan
712  Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f / Cam.Zoom;
713  moveSpeed.X = -moveSpeed.X;
714  cameraOffset += moveSpeed;
715  Vector2 max = new Vector2(GameMain.GraphicsWidth * 0.3f, GameMain.GraphicsHeight * 0.38f) / Cam.Zoom;
716  Vector2 min = -max;
717  cameraOffset = Vector2.Clamp(cameraOffset, min, max);
718  }
719  Cam.Position = targetPos + cameraOffset;
721  // Update widgets
722  jointSelectionWidgets.Values.ForEach(w => w.Update((float)deltaTime));
723  limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime));
724  animationWidgets.Values.ForEach(w => w.Update((float)deltaTime));
725  // Handle limb selection
726  if (PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == null && Widget.SelectedWidgets.None())
727  {
728  foreach (Limb limb in character.AnimController.Limbs)
729  {
730  if (limb == null || limb.ActiveSprite == null) { continue; }
731  if (selectedJoints.Any(j => j.LimbA == limb || j.LimbB == limb)) { continue; }
732  // Select limbs on ragdoll
733  if (editLimbs && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition))
734  {
735  HandleLimbSelection(limb);
736  }
737  // Select limbs on sprite sheet
738  if (GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition))
739  {
740  HandleLimbSelection(limb);
741  }
742  }
743  }
744  optionsToggle?.UpdateOpenState((float)deltaTime, new Vector2(-optionsPanel.Rect.Width - rightArea.RectTransform.AbsoluteOffset.X, 0), optionsPanel.RectTransform);
745  fileEditToggle?.UpdateOpenState((float)deltaTime, new Vector2(-fileEditPanel.Rect.Width - rightArea.RectTransform.AbsoluteOffset.X, 0), fileEditPanel.RectTransform);
746  characterPanelToggle?.UpdateOpenState((float)deltaTime, new Vector2(-characterSelectionPanel.Rect.Width - rightArea.RectTransform.AbsoluteOffset.X, 0), characterSelectionPanel.RectTransform);
747  minorModesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-minorModesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), minorModesPanel.RectTransform);
748  modesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-modesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), modesPanel.RectTransform);
749  buttonsPanelToggle?.UpdateOpenState((float)deltaTime, new Vector2(-buttonsPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), buttonsPanel.RectTransform);
750  totalMassText.Text = GetTotalMassText();
751  }
752 
753  private LocalizedString GetTotalMassText()
754  {
755  return TextManager.GetWithVariable($"{screenTextTag}totalmass", "[mass]", character?.AnimController?.Mass.FormatZeroDecimal() ?? "0");
756  }
757 
759  {
760  foreach (var limb in character.AnimController.Limbs)
761  {
762  if (limb?.ActiveSprite == null) { continue; }
763  if (selectedJoints.Any(j => j.LimbA == limb || j.LimbB == limb)) { continue; }
764  // character limbs
765  if (editLimbs && !spriteSheetRect.Contains(PlayerInput.MousePosition) &&
766  MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition)) { return CursorState.Hand; }
767  // spritesheet
768  if (showSpritesheet && GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; }
769  }
770  return CursorState.Default;
771  }
772 
776  private Vector2 scaledMouseSpeed;
777  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
778  {
779  if (isFrozen)
780  {
781  Timing.Alpha = 0.0f;
782  }
783  scaledMouseSpeed = PlayerInput.MouseSpeedPerSecond * (float)deltaTime;
784  Cam.UpdateTransform(true);
787 
788  // Lightmaps
789  if (GameMain.LightManager.LightingEnabled && Character.Controlled != null)
790  {
792  GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam);
793  GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition);
794  }
795  base.Draw(deltaTime, graphics, spriteBatch);
796 
797  graphics.Clear(backgroundColor);
798 
799  // Submarine
800  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: Cam.Transform);
801  Submarine.DrawBack(spriteBatch, editing: isEndlessRunner);
802  Submarine.DrawFront(spriteBatch, editing: isEndlessRunner);
803  spriteBatch.End();
804 
805  // Character(s)
806  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: Cam.Transform);
807  Character.CharacterList.ForEach(c => c.Draw(spriteBatch, Cam));
808  if (GameMain.DebugDraw)
809  {
810  character.AnimController.DebugDraw(spriteBatch);
811  }
812  else if (showColliders)
813  {
814  character.AnimController.Collider.DebugDraw(spriteBatch, Color.White, forceColor: true);
815  foreach (var limb in character.AnimController.Limbs)
816  {
817  if (!limb.Hide)
818  {
819  limb.body.DebugDraw(spriteBatch, GUIStyle.Green, forceColor: true);
820  }
821  }
822  }
823 
824  spriteBatch.End();
825 
826  // Lights
827  if (GameMain.LightManager.LightingEnabled)
828  {
829  spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null);
830  spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White);
831  spriteBatch.End();
832  }
833 
834  // GUI
835  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
836  if (drawDamageModifiers)
837  {
838  foreach (Limb limb in character.AnimController.Limbs)
839  {
840  if (selectedLimbs.Contains(limb) || selectedLimbs.None())
841  {
842  limb.DrawDamageModifiers(spriteBatch, cam, cam.WorldToScreen(limb.DrawPosition), isScreenSpace: true);
843  }
844  }
845  }
846  if (editAnimations)
847  {
848  DrawAnimationControls(spriteBatch, (float)deltaTime);
849  }
850  if (editLimbs)
851  {
852  DrawLimbEditor(spriteBatch);
853  }
854  if (drawSkeleton || editRagdoll || editJoints || editLimbs || editIK)
855  {
856  DrawRagdoll(spriteBatch, (float)deltaTime);
857  }
858  // Mouth
859  Limb head = character.AnimController.GetLimb(LimbType.Head);
860  if (head != null && character.CanEat && selectedLimbs.Contains(head))
861  {
862  var mouthPos = character.AnimController.GetMouthPosition();
863  if (mouthPos.HasValue)
864  {
865  ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUIStyle.Red, size: 8);
866  }
867  }
868  if (showSpritesheet)
869  {
870  DrawSpritesheetEditor(spriteBatch, (float)deltaTime);
871  }
872  if (isDrawingLimb)
873  {
874  GUI.DrawRectangle(spriteBatch, newLimbRect, Color.Yellow);
875  }
876  if (jointCreationMode != JointCreationMode.None)
877  {
878  var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 240, GameMain.GraphicsHeight / 4);
879  if (jointCreationMode == JointCreationMode.Select)
880  {
881  GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectAnchor1Pos"), Color.Yellow, font: GUIStyle.LargeFont);
882  }
883  else
884  {
885  GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUIStyle.LargeFont);
886  }
887  if (jointStartLimb != null && jointStartLimb.ActiveSprite != null)
888  {
889  GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointStartLimb), Color.Yellow, thickness: 3);
890  GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointStartLimb), Color.Yellow, thickness: 3);
891  }
892  if (jointEndLimb != null && jointEndLimb.ActiveSprite != null)
893  {
894  GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), GUIStyle.Green, thickness: 3);
895  GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), GUIStyle.Green, thickness: 3);
896  }
897  if (spriteSheetRect.Contains(PlayerInput.MousePosition))
898  {
899  if (jointStartLimb != null)
900  {
901  var startPos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2();
902  var offset = anchor1Pos ?? Vector2.Zero;
903  offset = -offset;
904  startPos += offset;
905  GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3);
906  }
907  }
908  else
909  {
910  if (jointStartLimb != null)
911  {
912  // TODO: there's something wrong here
913  var offset = anchor1Pos.HasValue ? Vector2.Transform(anchor1Pos.Value, Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero;
914  var startPos = cam.WorldToScreen(jointStartLimb.DrawPosition + offset);
915  GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3);
916  }
917  }
918  }
919  if (isDrawingLimb)
920  {
921  var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 200, GameMain.GraphicsHeight / 4);
922  GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("DrawLimbOnSpritesheet"), Color.Yellow, font: GUIStyle.LargeFont);
923  }
924  if (isEndlessRunner)
925  {
926  Vector2 indicatorPos = MiddleWall.Entities.First().DrawPosition;
927  GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.White);
928  }
929  GUI.Draw(Cam, spriteBatch);
930  if (isFrozen)
931  {
932  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUIStyle.LargeFont);
933  }
934  if (animTestPoseToggle.Selected)
935  {
936  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUIStyle.LargeFont);
937  }
938  if (selectedJoints.Count == 1)
939  {
940  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedJoints.First().Params.Name}", Color.White, font: GUIStyle.LargeFont);
941  }
942  if (selectedLimbs.Count == 1)
943  {
944  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUIStyle.LargeFont);
945  }
946  if (showSpritesheet)
947  {
948  Limb lastLimb = selectedLimbs.LastOrDefault();
949  if (lastLimb == null)
950  {
951  var lastJoint = selectedJoints.LastOrDefault();
952  if (lastJoint != null)
953  {
954  lastLimb = PlayerInput.KeyDown(Keys.LeftAlt) ? lastJoint.LimbB : lastJoint.LimbA;
955  }
956  }
957  if (lastLimb != null)
958  {
959  var topLeft = spriteSheetControls.RectTransform.TopLeft;
960  bool useSpritesheetOrientation = float.IsNaN(lastLimb.Params.SpriteOrientation);
961  GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation")+":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font);
962  float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation;
963  DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation,
964  GetCharacterEditorTranslation("spriteorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), useSpritesheetOrientation ? Color.White : Color.Yellow,
965  angle =>
966  {
967  TryUpdateSubParam(lastLimb.Params, "spriteorientation".ToIdentifier(), angle);
968  selectedLimbs.ForEach(l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle));
969  if (limbPairEditing)
970  {
971  UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle));
972  }
973  }, circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10);
974  }
975  else
976  {
977  var topLeft = spriteSheetControls.RectTransform.TopLeft;
978  GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUIStyle.Font);
979  DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation,
980  GetCharacterEditorTranslation("spritesheetorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), Color.White,
981  angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10);
982  }
983  }
984  // Debug
985  if (GameMain.DebugDraw)
986  {
987  // Limb positions
988  foreach (Limb limb in character.AnimController.Limbs)
989  {
990  Vector2 limbDrawPos = Cam.WorldToScreen(limb.WorldPosition);
991  GUI.DrawLine(spriteBatch, limbDrawPos + Vector2.UnitY * 5.0f, limbDrawPos - Vector2.UnitY * 5.0f, Color.White);
992  GUI.DrawLine(spriteBatch, limbDrawPos + Vector2.UnitX * 5.0f, limbDrawPos - Vector2.UnitX * 5.0f, Color.White);
993  }
994 
995  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 0), $"Cursor World Pos: {character.CursorWorldPosition}", Color.White, font: GUIStyle.SmallFont);
996  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"Cursor Pos: {character.CursorPosition}", Color.White, font: GUIStyle.SmallFont);
997  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 40), $"Cursor Screen Pos: {PlayerInput.MousePosition}", Color.White, font: GUIStyle.SmallFont);
998 
999  // Collider
1000  var collider = character.AnimController.Collider;
1001  var colliderDrawPos = SimToScreen(collider.SimPosition);
1002  Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation));
1003  var endPos = SimToScreen(collider.SimPosition + forward * collider.Radius);
1004  GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green);
1005  GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue);
1006  Vector2 left = forward.Left();
1007  GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + left * 0.25f), GUIStyle.Red);
1008  ShapeExtensions.DrawCircle(spriteBatch, colliderDrawPos, (endPos - colliderDrawPos).Length(), 40, GUIStyle.Green);
1009  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 300, 0), $"Collider rotation: {MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(collider.Rotation))}", Color.White, font: GUIStyle.SmallFont);
1010  }
1011  spriteBatch.End();
1012  }
1013 #endregion
1014 
1015 #region Ragdoll Manipulation
1016  private void UpdateJointCreation()
1017  {
1018  if (jointCreationMode == JointCreationMode.None)
1019  {
1020  jointStartLimb = null;
1021  jointEndLimb = null;
1022  anchor1Pos = null;
1023  return;
1024  }
1025  if (editJoints)
1026  {
1027  var selectedJoint = selectedJoints.LastOrDefault();
1028  if (selectedJoint != null)
1029  {
1030  if (jointCreationMode == JointCreationMode.Create)
1031  {
1032  if (spriteSheetRect.Contains(PlayerInput.MousePosition))
1033  {
1034  jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null);
1035  if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked())
1036  {
1037  Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero;
1038  anchor1.X = -anchor1.X;
1039  Vector2 anchor2 = (GetLimbSpritesheetRect(jointEndLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom;
1040  anchor2.X = -anchor2.X;
1041  CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2);
1042  jointCreationMode = JointCreationMode.None;
1043  }
1044  }
1045  else
1046  {
1047  jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null);
1048  if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked())
1049  {
1050  Vector2 anchor2 = ConvertUnits.ToDisplayUnits(jointEndLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition)));
1051  CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1Pos, anchor2);
1052  jointCreationMode = JointCreationMode.None;
1053  }
1054  }
1055  }
1056  else
1057  {
1058  jointStartLimb = selectedJoint.LimbB;
1059  if (spriteSheetRect.Contains(PlayerInput.MousePosition))
1060  {
1061  anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition;
1062  }
1063  else
1064  {
1065  anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition)));
1066  }
1067  if (PlayerInput.PrimaryMouseButtonClicked())
1068  {
1069  jointCreationMode = JointCreationMode.Create;
1070  }
1071  }
1072  }
1073  else
1074  {
1075  jointCreationMode = JointCreationMode.None;
1076  }
1077  }
1078  else if (editLimbs)
1079  {
1080  if (selectedLimbs.Any())
1081  {
1082  if (spriteSheetRect.Contains(PlayerInput.MousePosition))
1083  {
1084  if (jointCreationMode == JointCreationMode.Create)
1085  {
1086  jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden);
1087  if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked())
1088  {
1089  Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero;
1090  anchor1.X = -anchor1.X;
1091  Vector2 anchor2 = (GetLimbSpritesheetRect(jointEndLimb).Center.ToVector2() - PlayerInput.MousePosition) / spriteSheetZoom;
1092  anchor2.X = -anchor2.X;
1093  CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2);
1094  jointCreationMode = JointCreationMode.None;
1095  }
1096  }
1097  else if (PlayerInput.PrimaryMouseButtonClicked())
1098  {
1099  jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l));
1100  if (jointStartLimb != null)
1101  {
1102  anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition;
1103  jointCreationMode = JointCreationMode.Create;
1104  }
1105  }
1106  }
1107  else
1108  {
1109  if (jointCreationMode == JointCreationMode.Create)
1110  {
1111  jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden);
1112  if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked())
1113  {
1114  Vector2 anchor1 = anchor1Pos ?? Vector2.Zero;
1115  Vector2 anchor2 = ConvertUnits.ToDisplayUnits(jointEndLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition)));
1116  CreateJoint(jointStartLimb.Params.ID, jointEndLimb.Params.ID, anchor1, anchor2);
1117  jointCreationMode = JointCreationMode.None;
1118  }
1119  }
1120  else if (PlayerInput.PrimaryMouseButtonClicked())
1121  {
1122  jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden);
1123  if (jointStartLimb != null)
1124  {
1125  anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition)));
1126  jointCreationMode = JointCreationMode.Create;
1127  }
1128  }
1129  }
1130  }
1131  else
1132  {
1133  jointCreationMode = JointCreationMode.None;
1134  }
1135  }
1136  }
1137 
1138  private void UpdateLimbCreation()
1139  {
1140  if (!isDrawingLimb)
1141  {
1142  newLimbRect = Rectangle.Empty;
1143  return;
1144  }
1145  if (!editLimbs)
1146  {
1147  SetToggle(limbsToggle, true);
1148  }
1149  if (PlayerInput.PrimaryMouseButtonHeld())
1150  {
1151  if (newLimbRect == Rectangle.Empty)
1152  {
1153  newLimbRect = new Rectangle((int)PlayerInput.MousePosition.X, (int)PlayerInput.MousePosition.Y, 0, 0);
1154  }
1155  else
1156  {
1157  newLimbRect.Size = new Point((int)PlayerInput.MousePosition.X - newLimbRect.X, (int)PlayerInput.MousePosition.Y - newLimbRect.Y);
1158  }
1159  newLimbRect.Size = new Point(Math.Max(newLimbRect.Width, 2), Math.Max(newLimbRect.Height, 2));
1160  }
1161  if (PlayerInput.PrimaryMouseButtonClicked())
1162  {
1163  // Take the offset and the zoom into account
1164  newLimbRect.Location = new Point(newLimbRect.X - spriteSheetOffsetX, newLimbRect.Y - spriteSheetOffsetY);
1165  newLimbRect = newLimbRect.Divide(spriteSheetZoom);
1166  CreateNewLimb(newLimbRect);
1167  isDrawingLimb = false;
1168  newLimbRect = Rectangle.Empty;
1169  }
1170  }
1171 
1172  private void CopyLimb(Limb limb)
1173  {
1174  if (limb == null) { return; }
1175  // TODO: copy all params and sub params -> use a generic method/reflection?
1176  var rect = limb.ActiveSprite.SourceRect;
1177  var spriteParams = limb.Params.GetSprite();
1178  var newLimbElement = new XElement("limb",
1179  new XAttribute("id", RagdollParams.Limbs.Last().ID + 1),
1180  new XAttribute("radius", limb.Params.Radius),
1181  new XAttribute("width", limb.Params.Width),
1182  new XAttribute("height", limb.Params.Height),
1183  new XElement("sprite",
1184  new XAttribute("texture", spriteParams.Texture),
1185  new XAttribute("sourcerect", $"{rect.X}, {rect.Y}, {rect.Size.X}, {rect.Size.Y}"))).FromPackage(character.Prefab.ContentPackage);
1186  CreateLimb(newLimbElement);
1187  }
1188 
1189  private void CreateNewLimb(Rectangle sourceRect)
1190  {
1191  var newLimbElement = new XElement("limb",
1192  new XAttribute("id", RagdollParams.Limbs.Last().ID + 1),
1193  new XAttribute("width", sourceRect.Width * RagdollParams.TextureScale),
1194  new XAttribute("height", sourceRect.Height * RagdollParams.TextureScale),
1195  new XElement("sprite",
1196  new XAttribute("texture", RagdollParams.Limbs.First().GetSprite().Texture),
1197  new XAttribute("sourcerect", $"{sourceRect.X}, {sourceRect.Y}, {sourceRect.Width}, {sourceRect.Height}"))).FromPackage(character.Prefab.ContentPackage);
1198  CreateLimb(newLimbElement);
1199  lockSpriteOriginToggle.Selected = false;
1200  recalculateColliderToggle.Selected = true;
1201  }
1202 
1203  private void CreateLimb(ContentXElement newElement)
1204  {
1205  if (RagdollParams.MainElement == null)
1206  {
1207  DebugConsole.ThrowError("Main element null! Failed to create a limb.");
1208  return;
1209  }
1210  var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault();
1211  if (lastElement != null)
1212  {
1213  lastElement.AddAfterSelf(newElement);
1214  }
1215  else
1216  {
1217  RagdollParams.MainElement.AddFirst(newElement);
1218  }
1219  var newLimbParams = new RagdollParams.LimbParams(newElement, RagdollParams);
1220  RagdollParams.Limbs.Add(newLimbParams);
1221  character.AnimController.Recreate();
1222  CreateTextures();
1223  TeleportTo(spawnPosition);
1224  ClearWidgets();
1225  ClearSelection();
1226  selectedLimbs.Add(character.AnimController.Limbs.Single(l => l.Params == newLimbParams));
1227  ResetParamsEditor();
1228  }
1229 
1233  private void CreateJoint(int fromLimb, int toLimb, Vector2? anchor1 = null, Vector2? anchor2 = null)
1234  {
1235  if (RagdollParams.Joints.Any(j => j.Limb1 == fromLimb && j.Limb2 == toLimb))
1236  {
1237  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("ExistingJointFound").Replace("[limbid1]", fromLimb.ToString()).Replace("[limbid2]", toLimb.ToString()));
1238  return;
1239  }
1240  if (RagdollParams.MainElement == null)
1241  {
1242  DebugConsole.ThrowError("The main element of the ragdoll params is null! Failed to create a joint.");
1243  return;
1244  }
1245  //RagdollParams.StoreState();
1246  Vector2 a1 = anchor1 ?? Vector2.Zero;
1247  Vector2 a2 = anchor2 ?? Vector2.Zero;
1248  var newJointElement = new XElement("joint",
1249  new XAttribute("limb1", fromLimb),
1250  new XAttribute("limb2", toLimb),
1251  new XAttribute("limb1anchor", $"{a1.X.Format(2)}, {a1.Y.Format(2)}"),
1252  new XAttribute("limb2anchor", $"{a2.X.Format(2)}, {a2.Y.Format(2)}")
1253  ).FromPackage(character.Prefab.ContentPackage);
1254  var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault();
1255  if (lastJointElement == null)
1256  {
1257  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CantAddJointsNoLimbElements"));
1258  return;
1259  }
1260  lastJointElement.AddAfterSelf(newJointElement);
1261  var newJointParams = new RagdollParams.JointParams(newJointElement, RagdollParams);
1262  RagdollParams.Joints.Add(newJointParams);
1263  character.AnimController.Recreate();
1264  CreateTextures();
1265  TeleportTo(spawnPosition);
1266  ClearWidgets();
1267  ClearSelection();
1268  SetToggle(jointsToggle, true);
1269  selectedJoints.Add(character.AnimController.LimbJoints.Single(j => j.Params == newJointParams));
1270  }
1271 
1275  private void DeleteSelected()
1276  {
1277  //RagdollParams.StoreState();
1278  for (int i = 0; i < selectedJoints.Count; i++)
1279  {
1280  var joint = selectedJoints[i];
1281  joint.Params.Element.Remove();
1282  RagdollParams.Joints.Remove(joint.Params);
1283  }
1284  var removedIDs = new List<int>();
1285  for (int i = 0; i < selectedLimbs.Count; i++)
1286  {
1287  if (character.IsHumanoid)
1288  {
1289  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("HumanoidLimbDeletionDisabled"));
1290  break;
1291  }
1292  var limb = selectedLimbs[i];
1293  if (limb == character.AnimController.MainLimb)
1294  {
1295  DebugConsole.ThrowError("Can't remove the main limb, because it will cause unreveratable issues.");
1296  continue;
1297  }
1298  removedIDs.Add(limb.Params.ID);
1299  limb.Params.Element.Remove();
1300  RagdollParams.Limbs.Remove(limb.Params);
1301  }
1302  // Recreate ids
1303  var renamedIDs = new Dictionary<int, int>();
1304  for (int i = 0; i < RagdollParams.Limbs.Count; i++)
1305  {
1306  int oldID = RagdollParams.Limbs[i].ID;
1307  int newID = i;
1308  if (oldID != newID)
1309  {
1310  var limbParams = RagdollParams.Limbs[i];
1311  limbParams.ID = newID;
1312  limbParams.Name = limbParams.GenerateName();
1313  renamedIDs.Add(oldID, newID);
1314  }
1315  }
1316  // Refresh/recreate joints
1317  var jointsToRemove = new List<RagdollParams.JointParams>();
1318  for (int i = 0; i < RagdollParams.Joints.Count; i++)
1319  {
1320  var joint = RagdollParams.Joints[i];
1321  if (removedIDs.Contains(joint.Limb1) || removedIDs.Contains(joint.Limb2))
1322  {
1323  // At least one of the limbs has been removed -> remove the joint
1324  jointsToRemove.Add(joint);
1325  }
1326  else
1327  {
1328  // Both limbs still remains -> update
1329  bool rename = false;
1330  if (renamedIDs.TryGetValue(joint.Limb1, out int newID1))
1331  {
1332  joint.Limb1 = newID1;
1333  rename = true;
1334  }
1335  if (renamedIDs.TryGetValue(joint.Limb2, out int newID2))
1336  {
1337  joint.Limb2 = newID2;
1338  rename = true;
1339  }
1340  if (rename)
1341  {
1342  joint.Name = joint.GenerateName();
1343  }
1344  }
1345  }
1346  foreach (var jointParam in jointsToRemove)
1347  {
1348  jointParam.Element.Remove();
1349  RagdollParams.Joints.Remove(jointParam);
1350  }
1351  RecreateRagdoll();
1352  }
1353 #endregion
1354 
1355 #region Endless runner
1356  private int min;
1357  private int max;
1358  private void CalculateMovementLimits()
1359  {
1360  min = MiddleWall.Entities.Select(w => w.Rect.Left).OrderBy(p => p).First();
1361  max = MiddleWall.Entities.Select(w => w.Rect.Right).OrderBy(p => p).Last();
1362  }
1363 
1364  private readonly WallGroup[] wallGroups = new WallGroup[3];
1365 
1366  private WallGroup MiddleWall => wallGroups[1];
1367 
1368  private IEnumerable<MapEntity> AllStructures => wallGroups.SelectMany(c => c.Entities);
1369 
1370  private class WallGroup
1371  {
1372  public readonly List<MapEntity> Entities;
1373 
1374  public WallGroup(List<MapEntity> entities)
1375  {
1376  Entities = entities;
1377  }
1378 
1379  public WallGroup Clone()
1380  {
1381  var clones = new List<MapEntity>();
1382  Entities.ForEachMod(w => clones.Add(w.Clone()));
1383  return new WallGroup(clones);
1384  }
1385  }
1386 
1387  private void CloneWalls()
1388  {
1389  var originalWall = wallGroups[0];
1390  int moveAmount = originalWall.Entities.FirstOrDefault(e => e is Structure).Rect.Width;
1391  for (int i = 1; i <= 2; i++)
1392  {
1393  wallGroups[i] = originalWall.Clone();
1394  foreach (var entity in wallGroups[i].Entities)
1395  {
1396  entity.Move(new Vector2(moveAmount * i, 0));
1397  }
1398  }
1399  }
1400 
1401  private void UpdateWalls(bool right)
1402  {
1403  int moveAmount = wallGroups[0].Entities.FirstOrDefault(e => e is Structure).Rect.Width;
1404  int amount = right ? moveAmount : -moveAmount;
1405  foreach (var wallGroup in wallGroups)
1406  {
1407  foreach (var entity in wallGroup.Entities)
1408  {
1409  entity.Move(new Vector2(amount, 0));
1410  }
1411  }
1412 
1413  CalculateMovementLimits();
1414 
1415  GameMain.World.ProcessChanges();
1416  }
1417 
1418  private bool wallCollisionsEnabled;
1419  private void SetWallCollisions(bool enabled)
1420  {
1421  if (!isEndlessRunner) { return; }
1422  wallCollisionsEnabled = enabled;
1423  var collisionCategory = enabled ? FarseerPhysics.Dynamics.Category.Cat1 : FarseerPhysics.Dynamics.Category.None;
1424  AllStructures.ForEach(w => (w as Structure)?.SetCollisionCategory(collisionCategory));
1425  GameMain.World.ProcessChanges();
1426  }
1427 #endregion
1428 
1429 #region Character spawning
1430  private int characterIndex = -1;
1431  private Identifier currentCharacterIdentifier;
1432  private Identifier selectedJob = Identifier.Empty;
1433 
1434  private List<Identifier> visibleSpecies;
1435  private List<Identifier> VisibleSpecies
1436  {
1437  get
1438  {
1439  visibleSpecies ??= CharacterPrefab.Prefabs.Where(ShowCreature).OrderBy(p => p.Identifier).Select(p => p.Identifier).ToList();
1440  return visibleSpecies;
1441  }
1442  }
1443 
1444  private bool ShowCreature(CharacterPrefab prefab)
1445  {
1446  Identifier speciesName = prefab.Identifier;
1447  if (speciesName == CharacterPrefab.HumanSpeciesName) { return true; }
1448  if (!VanillaCharacters.Contains(prefab.ContentFile))
1449  {
1450  // Always show all custom characters.
1451  return true;
1452  }
1453  if (CreatureMetrics.UnlockAll) { return true; }
1454  return CreatureMetrics.Unlocked.Contains(speciesName);
1455  }
1456 
1457  private IEnumerable<CharacterFile> vanillaCharacters;
1458  private IEnumerable<CharacterFile> VanillaCharacters
1459  {
1460  get
1461  {
1462  vanillaCharacters ??= GameMain.VanillaContent.GetFiles<CharacterFile>();
1463  return vanillaCharacters;
1464  }
1465  }
1466 
1467  private Identifier GetNextCharacterIdentifier()
1468  {
1469  GetCurrentCharacterIndex();
1470  IncreaseIndex();
1471  currentCharacterIdentifier = VisibleSpecies[characterIndex];
1472  return currentCharacterIdentifier;
1473  }
1474 
1475  private Identifier GetPreviousCharacterIdentifier()
1476  {
1477  GetCurrentCharacterIndex();
1478  ReduceIndex();
1479  currentCharacterIdentifier = VisibleSpecies[characterIndex];
1480  return currentCharacterIdentifier;
1481  }
1482 
1483  private void GetCurrentCharacterIndex()
1484  {
1485  characterIndex = VisibleSpecies.IndexOf(character.SpeciesName);
1486  }
1487 
1488  private void IncreaseIndex()
1489  {
1490  characterIndex++;
1491  if (characterIndex > VisibleSpecies.Count - 1)
1492  {
1493  characterIndex = 0;
1494  }
1495  }
1496 
1497  private void ReduceIndex()
1498  {
1499  characterIndex--;
1500  if (characterIndex < 0)
1501  {
1502  characterIndex = VisibleSpecies.Count - 1;
1503  }
1504  }
1505 
1506  public Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null)
1507  {
1508  DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink);
1509  OnPreSpawn();
1510  bool followCursor = false;
1511  if (character != null)
1512  {
1513  followCursor = character.FollowCursor;
1515  CurrentAnimation.ClearHistory();
1516  if (!character.Removed)
1517  {
1518  character.Remove();
1519  }
1520  character = null;
1521  }
1522  if (speciesName == CharacterPrefab.HumanSpeciesName && !selectedJob.IsEmpty)
1523  {
1524  var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: JobPrefab.Prefabs[selectedJob.Value]);
1525  character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll);
1526  character.GiveJobItems(isPvPMode: false);
1527  HideWearables();
1528  if (displayWearables)
1529  {
1530  ShowWearables();
1531  }
1532  selectedJob = characterInfo.Job.Prefab.Identifier;
1533  }
1534  else
1535  {
1536  character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), hasAi: false, ragdoll: ragdoll);
1537  selectedJob = Identifier.Empty;
1538  }
1539  if (character != null)
1540  {
1541  character.FollowCursor = followCursor;
1542  }
1543  if (character == null)
1544  {
1545  if (currentCharacterIdentifier == speciesName)
1546  {
1547  return null;
1548  }
1549  else
1550  {
1551  // Respawn the current character;
1552  SpawnCharacter(currentCharacterIdentifier);
1553  }
1554  }
1555  OnPostSpawn();
1556  return character;
1557  }
1558 
1559  private void OnPreSpawn()
1560  {
1561  cameraOffset = Vector2.Zero;
1562  WayPoint wayPoint = null;
1563  if (!isEndlessRunner)
1564  {
1565  wayPoint = WayPoint.GetRandom(spawnType: SpawnType.Human, sub: Submarine.MainSub);
1566  }
1567  wayPoint ??= WayPoint.GetRandom(sub: Submarine.MainSub);
1568  spawnPosition = wayPoint.WorldPosition;
1569  }
1570 
1571  private void OnPostSpawn()
1572  {
1573  currentCharacterIdentifier = character.SpeciesName;
1574  GetCurrentCharacterIndex();
1575  character.Submarine = Submarine.MainSub;
1576  character.AnimController.forceStanding = character.AnimController.CanWalk;
1577  character.AnimController.ForceSelectAnimationType = character.AnimController.CanWalk ? AnimationType.Walk : AnimationType.SwimSlow;
1578  Character.Controlled = character;
1579  SetWallCollisions(character.AnimController.forceStanding);
1580  CreateTextures();
1581  CreateGUI();
1582  ClearWidgets();
1583  ClearSelection();
1584  ResetParamsEditor();
1585  CurrentAnimation.StoreSnapshot();
1586  RagdollParams.StoreSnapshot();
1587  Cam.Position = character.WorldPosition;
1588  editedCharacters.Add(character);
1589  }
1590 
1591  private void ClearWidgets()
1592  {
1593  Widget.SelectedWidgets.Clear();
1594  animationWidgets.Clear();
1595  jointSelectionWidgets.Clear();
1596  limbEditWidgets.Clear();
1597  }
1598 
1599  private void ClearSelection()
1600  {
1601  selectedLimbs.Clear();
1602  selectedJoints.Clear();
1603  foreach (var w in jointSelectionWidgets.Values)
1604  {
1605  w.Refresh();
1606  w.LinkedWidget?.Refresh();
1607  }
1608  }
1609 
1610  private void RecreateRagdoll(RagdollParams ragdoll = null)
1611  {
1612  RagdollParams.Apply();
1613  character.AnimController.Recreate(ragdoll);
1614  TeleportTo(spawnPosition);
1615  // For some reason Enumerable.Contains() method does not find the match, threfore the conversion to a list.
1616  var selectedJointParams = selectedJoints.Select(j => j.Params).ToList();
1617  var selectedLimbParams = selectedLimbs.Select(l => l.Params).ToList();
1618  CreateTextures();
1619  ClearWidgets();
1620  ClearSelection();
1621  foreach (var joint in character.AnimController.LimbJoints)
1622  {
1623  if (selectedJointParams.Contains(joint.Params))
1624  {
1625  selectedJoints.Add(joint);
1626  }
1627  }
1628  foreach (var limb in character.AnimController.Limbs)
1629  {
1630  if (selectedLimbParams.Contains(limb.Params))
1631  {
1632  selectedLimbs.Add(limb);
1633  }
1634  }
1635  ResetParamsEditor();
1636  }
1637 
1638  private void TeleportTo(Vector2 position)
1639  {
1640  if (isEndlessRunner)
1641  {
1642  character.AnimController.SetPosition(ConvertUnits.ToSimUnits(position), false);
1643  }
1644  else
1645  {
1646  character.TeleportTo(position);
1647  }
1648  Cam.Position = character.WorldPosition;
1649  }
1650 
1651  public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable<AnimationParams> animations = null)
1652  {
1653  if (name.IsEmpty)
1654  {
1655  throw new ArgumentException("Name cannot be empty.");
1656  }
1657 
1658  var vanilla = GameMain.VanillaContent;
1659 
1660  if (contentPackage == null)
1661  {
1662  contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(cp => cp != vanilla);
1663  }
1664  if (contentPackage == null)
1665  {
1666  // This should not be possible.
1667  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("NoContentPackageSelected"));
1668  return false;
1669  }
1670  if (vanilla != null && contentPackage == vanilla)
1671  {
1672  GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont);
1673  return false;
1674  }
1675  // Content package
1676  if (contentPackage is RegularPackage regular && !ContentPackageManager.EnabledPackages.Regular.Contains(regular))
1677  {
1678  ContentPackageManager.EnabledPackages.EnableRegular(regular);
1679  }
1680  GameSettings.SaveCurrentConfig();
1681 
1682  // Config file
1683  string configFilePath = Path.Combine(mainFolder, $"{name}.xml").Replace(@"\", @"/");
1684  var duplicate = CharacterPrefab.ConfigElements.FirstOrDefault(e => e.GetAttributeIdentifier("speciesname", Identifier.Empty) == name);
1685  XElement overrideElement = null;
1686  if (duplicate != null)
1687  {
1688  visibleSpecies = null;
1689  if (!File.Exists(configFilePath))
1690  {
1691  // If the file exists, we just want to overwrite it.
1692  // If the file does not exist, it's part of a different content package -> we'll want to override it.
1693  overrideElement = new XElement("override");
1694  }
1695  }
1696 
1697  if (config == null)
1698  {
1699  config = new XElement("Character",
1700  new XAttribute("speciesname", name),
1701  new XAttribute("humanoid", isHumanoid),
1702  new XElement("ragdolls", CreateRagdollPath()),
1703  new XElement("animations", CreateAnimationPath()),
1704  new XElement("health"),
1705  new XElement("ai"));
1706  }
1707  else
1708  {
1709  config.TrySetAttributeValue("speciesname", name);
1710  config.TrySetAttributeValue("humanoid", isHumanoid);
1711  var ragdollElement = config.GetChildElement("ragdolls");
1712  if (ragdollElement == null)
1713  {
1714  config.Add(new XElement("ragdolls", CreateRagdollPath()));
1715  }
1716  else
1717  {
1718  var path = ragdollElement.GetAttributeString("folder", "");
1719  if (!string.IsNullOrEmpty(path) && !path.Equals("default", StringComparison.OrdinalIgnoreCase))
1720  {
1721  ragdollElement.ReplaceWith(new XElement("ragdolls", CreateRagdollPath()));
1722  }
1723  }
1724  var animationElement = config.GetChildElement("animations");
1725  if (animationElement == null)
1726  {
1727  config.Add(new XElement("animations", CreateAnimationPath()));
1728  }
1729  else
1730  {
1731  var path = animationElement.GetAttributeString("folder", "");
1732  if (!string.IsNullOrEmpty(path) && !path.Equals("default", StringComparison.OrdinalIgnoreCase))
1733  {
1734  animationElement.ReplaceWith(new XElement("animations", CreateAnimationPath()));
1735  }
1736  }
1737  }
1738 
1739  XAttribute CreateRagdollPath() => new XAttribute("folder", Path.Combine(mainFolder, $"Ragdolls/").Replace(@"\", @"/"));
1740  XAttribute CreateAnimationPath() => new XAttribute("folder", Path.Combine(mainFolder, $"Animations/").Replace(@"\", @"/"));
1741 
1742  if (overrideElement != null)
1743  {
1744  overrideElement.Add(config);
1745  config = overrideElement;
1746  }
1747  XDocument doc = new XDocument(config);
1748 
1749  ContentPath configFileContentPath = ContentPath.FromRaw(contentPackage, configFilePath);
1750  Directory.CreateDirectory(Path.GetDirectoryName(configFileContentPath.Value));
1751 #if DEBUG
1752  doc.Save(configFileContentPath.Value);
1753 #else
1754  doc.SaveSafe(configFileContentPath.Value);
1755 #endif
1756  // Add to the selected content package
1757  var modProject = new ModProject(contentPackage);
1758  var newFile = ModProject.File.FromPath<CharacterFile>(configFilePath);
1759  modProject.AddFile(newFile);
1760  modProject.Save(contentPackage.Path);
1761 
1762  var reloadResult = ContentPackageManager.ReloadContentPackage(contentPackage);
1763  if (!reloadResult.TryUnwrapSuccess(out var newPackage))
1764  {
1765  throw new Exception($"Failed to reload package",
1766  reloadResult.TryUnwrapFailure(out var exception) ? exception : null);
1767  }
1768  contentPackage = newPackage;
1769 
1770  DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path));
1771 
1772  // Ragdoll
1774  string ragdollPath = RagdollParams.GetDefaultFile(name, contentPackage);
1775  RagdollParams ragdollParams = isHumanoid
1776  ? RagdollParams.CreateDefault<HumanRagdollParams>(ragdollPath, name, ragdoll)
1777  : RagdollParams.CreateDefault<FishRagdollParams>(ragdollPath, name, ragdoll);
1778 
1779  // Animations
1781  string animFolder = AnimationParams.GetFolder(name);
1782  if (animations != null)
1783  {
1784  if (!Directory.Exists(animFolder))
1785  {
1786  Directory.CreateDirectory(animFolder);
1787  }
1788  foreach (var animation in animations)
1789  {
1790  XElement element = animation.MainElement;
1791  if (element == null) { continue; }
1792  element.SetAttributeValue("type", name);
1793  string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType);
1794  element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType);
1795 #if DEBUG
1796  element.Save(fullPath);
1797 #else
1798  element.SaveSafe(fullPath);
1799 #endif
1800  }
1801  }
1802  else
1803  {
1804  foreach (AnimationType animType in Enum.GetValues(typeof(AnimationType)))
1805  {
1806  switch (animType)
1807  {
1808  case AnimationType.Walk:
1809  case AnimationType.Run:
1810  if (!ragdollParams.CanWalk) { continue; }
1811  break;
1812  case AnimationType.Crouch:
1813  if (!ragdollParams.CanWalk || !isHumanoid) { continue; }
1814  break;
1815  case AnimationType.SwimSlow:
1816  case AnimationType.SwimFast:
1817  break;
1818  default: continue;
1819  }
1820  Type type = AnimationParams.GetParamTypeFromAnimType(animType, isHumanoid);
1821  string fullPath = AnimationParams.GetDefaultFile(name, animType);
1822  AnimationParams.Create(fullPath, name, animType, type);
1823  }
1824  }
1825  if (!VisibleSpecies.Contains(name))
1826  {
1827  VisibleSpecies.Add(name);
1828  }
1829  SpawnCharacter(name, ragdollParams);
1830  limbPairEditing = false;
1831  limbsToggle.Selected = true;
1832  recalculateColliderToggle.Selected = true;
1833  lockSpriteOriginToggle.Selected = false;
1834  selectedLimbs.Add(character.AnimController.Limbs.First());
1835  return true;
1836  }
1837 
1838  private void ShowWearables()
1839  {
1840  if (character.Inventory == null) { return; }
1841  foreach (var item in character.Inventory.AllItems)
1842  {
1843  // Temp condition, todo: remove
1844  if (item.AllowedSlots.Contains(InvSlotType.Head) || item.AllowedSlots.Contains(InvSlotType.Headset)) { continue; }
1845  item.Equip(character);
1846  }
1847  }
1848 
1849  private void HideWearables()
1850  {
1851  character.Inventory?.AllItemsMod.ForEach(i => i.Unequip(character));
1852  }
1853 #endregion
1854 
1855 #region GUI
1856  private static Vector2 innerScale = new Vector2(0.95f, 0.95f);
1857 
1858  private GUILayoutGroup rightArea, leftArea;
1859  private GUIFrame centerArea;
1860 
1861  private GUITextBlock totalMassText;
1862  private GUIFrame characterSelectionPanel;
1863  private GUIFrame fileEditPanel;
1864  private GUIFrame modesPanel;
1865  private GUIFrame buttonsPanel;
1866  private GUIFrame optionsPanel;
1867  private GUIFrame minorModesPanel;
1868 
1869  private GUIFrame ragdollControls;
1870  private GUIFrame jointControls;
1871  private GUIFrame animationControls;
1872  private GUIFrame limbControls;
1873  private GUIFrame spriteSheetControls;
1874  private GUIFrame backgroundColorPanel;
1875 
1876  private GUIDropDown animSelection;
1877  private GUITickBox freezeToggle;
1878  private GUITickBox animTestPoseToggle;
1879  private GUITickBox showCollidersToggle;
1880  private GUIScrollBar jointScaleBar;
1881  private GUIScrollBar limbScaleBar;
1882  private GUIScrollBar spriteSheetZoomBar;
1883  private GUITickBox copyJointsToggle;
1884  private GUITickBox recalculateColliderToggle;
1885  private GUIFrame resetSpriteOrientationButtonParent;
1886 
1887  private GUITickBox characterInfoToggle;
1888  private GUITickBox ragdollToggle;
1889  private GUITickBox animsToggle;
1890  private GUITickBox limbsToggle;
1891  private GUITickBox paramsToggle;
1892  private GUITickBox jointsToggle;
1893  private GUITickBox spritesheetToggle;
1894  private GUITickBox skeletonToggle;
1895  private GUITickBox lightsToggle;
1896  private GUITickBox damageModifiersToggle;
1897  private GUITickBox ikToggle;
1898  private GUITickBox lockSpriteOriginToggle;
1899 
1900  private GUIFrame extraRagdollControls;
1901  private GUIButton createJointButton;
1902  private GUIButton createLimbButton;
1903  private GUIButton deleteSelectedButton;
1904  private GUIButton duplicateLimbButton;
1905 
1906  private ToggleButton modesToggle;
1907  private ToggleButton minorModesToggle;
1908  private ToggleButton buttonsPanelToggle;
1909  private ToggleButton optionsToggle;
1910  private ToggleButton characterPanelToggle;
1911  private ToggleButton fileEditToggle;
1912 
1913  private void CreateGUI()
1914  {
1915  // Release the old areas
1916  if (rightArea != null)
1917  {
1918  rightArea.RectTransform.Parent = null;
1919  }
1920  if (centerArea != null)
1921  {
1922  centerArea.RectTransform.Parent = null;
1923  }
1924  if (leftArea != null)
1925  {
1926  leftArea.RectTransform.Parent = null;
1927  }
1928 
1929  // Create the areas
1930  rightArea = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1.0f), parent: Frame.RectTransform, anchor: Anchor.CenterRight), childAnchor: Anchor.BottomRight)
1931  {
1932  RelativeSpacing = 0.02f
1933  };
1934  centerArea = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.95f), parent: Frame.RectTransform, anchor: Anchor.TopRight)
1935  {
1936  AbsoluteOffset = new Point((int)(rightArea.RectTransform.ScaledSize.X + rightArea.RectTransform.RelativeOffset.X * rightArea.RectTransform.Parent.ScaledSize.X + (int)(20 * GUI.xScale)), (int)(20 * GUI.yScale))
1937  }, style: null)
1938  { CanBeFocused = false };
1939  leftArea = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 0.95f), parent: Frame.RectTransform, anchor: Anchor.CenterLeft), childAnchor: Anchor.BottomLeft)
1940  {
1941  RelativeSpacing = 0.02f
1942  };
1943  Vector2 toggleSize = new Vector2(1.0f, 0.03f);
1944  CreateFileEditPanel();
1945  CreateOptionsPanel(toggleSize);
1946  CreateCharacterSelectionPanel();
1947  if (rightArea.RectTransform.Children.Sum(c => c.Rect.Height) > GameMain.GraphicsHeight)
1948  {
1949  fileEditPanel.GetAllChildren().Where(c => c is GUIButton).ForEach(b => b.RectTransform.MinSize = ((GUIButton)b).Frame.RectTransform.MinSize = b.RectTransform.MinSize.Multiply(new Vector2(1.0f, 0.75f)));
1950  fileEditPanel.RectTransform.MinSize = new Point(0, (int)(fileEditPanel.GetChild<GUILayoutGroup>().RectTransform.Children.Sum(c => c.Rect.Height) / innerScale.Y));
1951  optionsPanel.GetAllChildren().Where(c => c is GUITickBox).ForEach(t => t.RectTransform.MinSize = t.RectTransform.MinSize.Multiply(new Vector2(1.0f, 0.75f)));
1952  optionsPanel.RectTransform.MinSize = new Point(0, (int)(optionsPanel.GetChild<GUILayoutGroup>().RectTransform.Children.Sum(c => c.Rect.Height) / innerScale.Y));
1953  rightArea.Recalculate();
1954  }
1955  CreateButtonsPanel();
1956  CreateModesPanel(toggleSize);
1957  CreateMinorModesPanel(toggleSize);
1958  CreateContextualControls();
1959  totalMassText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.01f), rightArea.RectTransform), GetTotalMassText());
1960  }
1961 
1962  private void CreateMinorModesPanel(Vector2 toggleSize)
1963  {
1964  minorModesPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftArea.RectTransform));
1965  var layoutGroup = new GUILayoutGroup(new RectTransform(innerScale, minorModesPanel.RectTransform, Anchor.Center))
1966  {
1967  AbsoluteSpacing = 2,
1968  Stretch = true
1969  };
1970  new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUIStyle.LargeFont);
1971  paramsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowParameters")) { Selected = showParamsEditor };
1972  paramsToggle.OnSelected = box =>
1973  {
1974  showParamsEditor = box.Selected;
1975  return true;
1976  };
1977  spritesheetToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowSpriteSheet")) { Selected = showSpritesheet };
1978  spritesheetToggle.OnSelected = box =>
1979  {
1980  showSpritesheet = box.Selected;
1981  return true;
1982  };
1983  showCollidersToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowColliders"))
1984  {
1985  Selected = showColliders,
1986  OnSelected = box =>
1987  {
1988  showColliders = box.Selected;
1989  return true;
1990  }
1991  };
1992  ikToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditIKTargets")) { Selected = editIK };
1993  ikToggle.OnSelected = box =>
1994  {
1995  editIK = box.Selected;
1996  return true;
1997  };
1998  skeletonToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("DrawSkeleton")) { Selected = drawSkeleton };
1999  skeletonToggle.OnSelected = box =>
2000  {
2001  drawSkeleton = box.Selected;
2002  return true;
2003  };
2004  lightsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EnableLights")) { Selected = GameMain.LightManager.LightingEnabled };
2005  lightsToggle.OnSelected = box =>
2006  {
2007  GameMain.LightManager.LightingEnabled = box.Selected;
2008  return true;
2009  };
2010  damageModifiersToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("DrawDamageModifiers")) { Selected = drawDamageModifiers };
2011  damageModifiersToggle.OnSelected = box =>
2012  {
2013  drawDamageModifiers = box.Selected;
2014  return true;
2015  };
2016  minorModesToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), minorModesPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left);
2017  minorModesPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f));
2018  }
2019 
2020  private void CreateModesPanel(Vector2 toggleSize)
2021  {
2022  modesPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), leftArea.RectTransform));
2023  var layoutGroup = new GUILayoutGroup(new RectTransform(innerScale, modesPanel.RectTransform, Anchor.Center))
2024  {
2025  AbsoluteSpacing = 2,
2026  Stretch = true
2027  };
2028  new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUIStyle.LargeFont);
2029  characterInfoToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditCharacter")) { Selected = editCharacterInfo };
2030  ragdollToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditRagdoll")) { Selected = editRagdoll };
2031  limbsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditLimbs")) { Selected = editLimbs };
2032  jointsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditJoints")) { Selected = editJoints };
2033  animsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditAnimations")) { Selected = editAnimations };
2034  animsToggle.OnSelected = box =>
2035  {
2036  editAnimations = box.Selected;
2037  if (editAnimations)
2038  {
2039  SetToggle(limbsToggle, false);
2040  SetToggle(jointsToggle, false);
2041  SetToggle(ragdollToggle, false);
2042  SetToggle(characterInfoToggle, false);
2043  spritesheetToggle.Selected = false;
2044  }
2045  ClearSelection();
2046  ResetParamsEditor();
2047  return true;
2048  };
2049  limbsToggle.OnSelected = box =>
2050  {
2051  editLimbs = box.Selected;
2052  if (editLimbs)
2053  {
2054  SetToggle(animsToggle, false);
2055  SetToggle(jointsToggle, false);
2056  SetToggle(ragdollToggle, false);
2057  SetToggle(characterInfoToggle, false);
2058  spritesheetToggle.Selected = true;
2059  }
2060  ClearSelection();
2061  ResetParamsEditor();
2062  return true;
2063  };
2064  jointsToggle.OnSelected = box =>
2065  {
2066  editJoints = box.Selected;
2067  if (editJoints)
2068  {
2069  SetToggle(limbsToggle, false);
2070  SetToggle(animsToggle, false);
2071  SetToggle(ragdollToggle, false);
2072  SetToggle(characterInfoToggle, false);
2073  ikToggle.Selected = false;
2074  spritesheetToggle.Selected = true;
2075  }
2076  ClearSelection();
2077  ResetParamsEditor();
2078  return true;
2079  };
2080  ragdollToggle.OnSelected = box =>
2081  {
2082  editRagdoll = box.Selected;
2083  if (editRagdoll)
2084  {
2085  SetToggle(limbsToggle, false);
2086  SetToggle(animsToggle, false);
2087  SetToggle(jointsToggle, false);
2088  SetToggle(characterInfoToggle, false);
2089  paramsToggle.Selected = true;
2090  }
2091  ClearSelection();
2092  ResetParamsEditor();
2093  return true;
2094  };
2095  characterInfoToggle.OnSelected = box =>
2096  {
2097  editCharacterInfo = box.Selected;
2098  if (editCharacterInfo)
2099  {
2100  SetToggle(limbsToggle, false);
2101  SetToggle(animsToggle, false);
2102  SetToggle(ragdollToggle, false);
2103  SetToggle(jointsToggle, false);
2104  paramsToggle.Selected = true;
2105  }
2106  ClearSelection();
2107  ResetParamsEditor();
2108  return true;
2109  };
2110  modesToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), modesPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left);
2111  modesPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f));
2112  }
2113 
2114  private void SetToggle(GUITickBox toggle, bool value)
2115  {
2116  if (toggle.Selected != value)
2117  {
2118  if (value)
2119  {
2120  toggle.Box.Flash(GUIStyle.Green, useRectangleFlash: true);
2121  }
2122  else
2123  {
2124  toggle.Box.Flash(GUIStyle.Red, useRectangleFlash: true);
2125  }
2126  }
2127  toggle.Selected = value;
2128  }
2129 
2130  private void CreateButtonsPanel()
2131  {
2132  buttonsPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), leftArea.RectTransform));
2133  Vector2 buttonSize = new Vector2(1, 0.45f);
2134  var parent = new GUIFrame(new RectTransform(new Vector2(0.85f, 0.70f), buttonsPanel.RectTransform, Anchor.Center), style: null);
2135  var reloadTexturesButton = new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.TopCenter), GetCharacterEditorTranslation("ReloadTextures"));
2136  reloadTexturesButton.OnClicked += (button, userData) =>
2137  {
2138  foreach (var limb in character.AnimController.Limbs)
2139  {
2140  if (limb == null) { continue; }
2141  limb.ActiveSprite?.ReloadTexture();
2142  limb.WearingItems.ForEach(i => i.Sprite.ReloadTexture());
2143  limb.OtherWearables.ForEach(w => w.Sprite.ReloadTexture());
2144  }
2145  CreateTextures();
2146  return true;
2147  };
2148  var recreateButton = new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll"))
2149  {
2150  ToolTip = GetCharacterEditorTranslation("RecreateRagdollTooltip"),
2151  OnClicked = (button, data) =>
2152  {
2153  RecreateRagdoll();
2154  character.AnimController.ResetLimbs();
2155  return true;
2156  }
2157  };
2158  GUITextBlock.AutoScaleAndNormalize(reloadTexturesButton.TextBlock, recreateButton.TextBlock);
2159  buttonsPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), buttonsPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left);
2160  buttonsPanel.RectTransform.MinSize = new Point(0, (int)(parent.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.5f));
2161  }
2162 
2163 
2164  private void CreateOptionsPanel(Vector2 toggleSize)
2165  {
2166  optionsPanel = new GUIFrame(new RectTransform(new Vector2(1, 0.3f), rightArea.RectTransform));
2167  var layoutGroup = new GUILayoutGroup(new RectTransform(innerScale, optionsPanel.RectTransform, Anchor.Center))
2168  {
2169  AbsoluteSpacing = 2,
2170  Stretch = true
2171  };
2172  new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUIStyle.LargeFont);
2173  freezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("Freeze"))
2174  {
2175  Selected = isFrozen,
2176  OnSelected = box =>
2177  {
2178  isFrozen = box.Selected;
2179  return true;
2180  }
2181  };
2182  new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AutoFreeze"))
2183  {
2184  Selected = autoFreeze,
2185  OnSelected = box =>
2186  {
2187  autoFreeze = box.Selected;
2188  return true;
2189  }
2190  };
2191  new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LimbPairEditing"))
2192  {
2193  Selected = limbPairEditing,
2194  Enabled = character.IsHumanoid,
2195  OnSelected = box =>
2196  {
2197  limbPairEditing = box.Selected;
2198  return true;
2199  }
2200  };
2201  animTestPoseToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AnimationTestPose"))
2202  {
2203  Selected = character.AnimController.AnimationTestPose,
2204  Enabled = true,
2205  OnSelected = box =>
2206  {
2207  character.AnimController.AnimationTestPose = box.Selected;
2208  return true;
2209  }
2210  };
2211  new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AutoMove"))
2212  {
2213  Selected = character.OverrideMovement != null,
2214  OnSelected = box =>
2215  {
2216  character.OverrideMovement = box.Selected ? new Vector2(1, 0) as Vector2? : null;
2217  return true;
2218  }
2219  };
2220  new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("FollowCursor"))
2221  {
2222  Selected = character.FollowCursor,
2223  OnSelected = box =>
2224  {
2225  character.FollowCursor = box.Selected;
2226  return true;
2227  }
2228  };
2229  new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditBackgroundColor"))
2230  {
2231  Selected = displayBackgroundColor,
2232  OnSelected = box =>
2233  {
2234  displayBackgroundColor = box.Selected;
2235  return true;
2236  }
2237  };
2238  optionsToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), optionsPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right);
2239  optionsPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f));
2240  }
2241 
2242  private void CreateContextualControls()
2243  {
2244  Point elementSize = new Point(120, 20).Multiply(GUI.Scale);
2245  int textAreaHeight = 20;
2246  // General controls
2247  backgroundColorPanel = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), centerArea.RectTransform, Anchor.TopRight)
2248  {
2249  AbsoluteOffset = new Point(10, 0).Multiply(GUI.Scale)
2250  }, style: null)
2251  {
2252  CanBeFocused = false
2253  };
2254  // Background color
2255  var frame = new GUIFrame(new RectTransform(new Point(500, 80).Multiply(GUI.Scale), backgroundColorPanel.RectTransform, Anchor.TopRight), style: null, color: Color.Black * 0.4f);
2256  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform)
2257  {
2258  MinSize = new Point(80, 26)
2259  }, GetCharacterEditorTranslation("BackgroundColor") + ":", textColor: Color.WhiteSmoke);
2260  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1), frame.RectTransform, Anchor.TopRight)
2261  {
2262  AbsoluteOffset = new Point(20, 0).Multiply(GUI.Scale)
2263  }, isHorizontal: true, childAnchor: Anchor.CenterRight)
2264  {
2265  Stretch = true,
2266  RelativeSpacing = 0.01f
2267  };
2268  var fields = new GUIComponent[4];
2269  string[] colorComponentLabels = { "R", "G", "B" };
2270  for (int i = 2; i >= 0; i--)
2271  {
2272  var element = new GUIFrame(new RectTransform(new Vector2(0.3f, 1), inputArea.RectTransform)
2273  {
2274  MinSize = new Point(40, 0),
2275  MaxSize = new Point(100, 50)
2276  }, style: null, color: Color.Black * 0.6f);
2277  var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i],
2278  font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
2279  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
2280  NumberType.Int, relativeButtonAreaWidth: 0.25f)
2281  {
2282  Font = GUIStyle.SmallFont
2283  };
2284  numberInput.MinValueInt = 0;
2285  numberInput.MaxValueInt = 255;
2286  numberInput.Font = GUIStyle.SmallFont;
2287  switch (i)
2288  {
2289  case 0:
2290  colorLabel.TextColor = GUIStyle.Red;
2291  numberInput.IntValue = backgroundColor.R;
2292  numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)numInput.IntValue;
2293  break;
2294  case 1:
2295  colorLabel.TextColor = GUIStyle.Green;
2296  numberInput.IntValue = backgroundColor.G;
2297  numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)numInput.IntValue;
2298  break;
2299  case 2:
2300  colorLabel.TextColor = Color.DeepSkyBlue;
2301  numberInput.IntValue = backgroundColor.B;
2302  numberInput.OnValueChanged += (numInput) => backgroundColor.B = (byte)numInput.IntValue;
2303  break;
2304  }
2305  }
2306  // Spritesheet controls
2307  spriteSheetControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), centerArea.RectTransform, Anchor.BottomLeft)
2308  {
2309  RelativeOffset = new Vector2(0, 0.1f)
2310  }, style: null)
2311  {
2312  CanBeFocused = false
2313  };
2314  var layoutGroupSpriteSheet = new GUILayoutGroup(new RectTransform(Vector2.One, spriteSheetControls.RectTransform))
2315  {
2316  AbsoluteSpacing = 5,
2317  CanBeFocused = false
2318  };
2319  new GUITextBlock(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), GetCharacterEditorTranslation("SpriteSheetZoom") + ":", Color.White);
2320  var spriteSheetControlElement = new GUIFrame(new RectTransform(new Point(elementSize.X * 2, textAreaHeight), layoutGroupSpriteSheet.RectTransform), style: null);
2321  CalculateSpritesheetZoom();
2322  spriteSheetZoomBar = new GUIScrollBar(new RectTransform(new Vector2(0.69f, 1), spriteSheetControlElement.RectTransform, Anchor.CenterLeft), barSize: 0.2f, style: "GUISlider")
2323  {
2324  BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom)),
2325  Step = 0.01f,
2326  OnMoved = (scrollBar, value) =>
2327  {
2328  spriteSheetZoom = MathHelper.Lerp(spriteSheetMinZoom, spriteSheetMaxZoom, value);
2329  return true;
2330  }
2331  };
2332  new GUIButton(new RectTransform(new Vector2(0.3f, 1.25f), spriteSheetControlElement.RectTransform, Anchor.CenterRight), GetCharacterEditorTranslation("Reset"), style: "GUIButtonFreeScale")
2333  {
2334  OnClicked = (box, data) =>
2335  {
2336  spriteSheetZoom = Math.Min(1, spriteSheetMaxZoom);
2337  spriteSheetZoomBar.BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom));
2338  return true;
2339  }
2340  };
2341  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), GetCharacterEditorTranslation("HideBodySprites"))
2342  {
2343  TextColor = Color.White,
2344  Selected = hideBodySheet,
2345  OnSelected = (GUITickBox box) =>
2346  {
2347  hideBodySheet = box.Selected;
2348  return true;
2349  }
2350  };
2351  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), GetCharacterEditorTranslation("ShowWearables"))
2352  {
2353  TextColor = Color.White,
2354  Selected = displayWearables,
2355  OnSelected = (GUITickBox box) =>
2356  {
2357  displayWearables = box.Selected;
2358  if (displayWearables)
2359  {
2360  ShowWearables();
2361  }
2362  else
2363  {
2364  HideWearables();
2365  }
2366  return true;
2367  }
2368  };
2369  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupSpriteSheet.RectTransform), GetCharacterEditorTranslation("Unrestrict"))
2370  {
2371  TextColor = Color.White,
2372  Selected = unrestrictSpritesheet,
2373  OnSelected = (GUITickBox box) =>
2374  {
2375  SetSpritesheetRestriction(box.Selected);
2376  return true;
2377  }
2378  };
2379  resetSpriteOrientationButtonParent = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.025f), centerArea.RectTransform, Anchor.BottomCenter)
2380  {
2381  AbsoluteOffset = new Point(0, -5).Multiply(GUI.Scale),
2382  RelativeOffset = new Vector2(-0.05f, 0)
2383  }, style: null)
2384  {
2385  CanBeFocused = false
2386  };
2387  new GUIButton(new RectTransform(Vector2.One, resetSpriteOrientationButtonParent.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("Reset"), style: "GUIButtonFreeScale")
2388  {
2389  OnClicked = (box, data) =>
2390  {
2391  IEnumerable<Limb> limbs = selectedLimbs;
2392  if (limbs.None())
2393  {
2394  limbs = selectedJoints.Select(j => PlayerInput.KeyDown(Keys.LeftAlt) ? j.LimbB : j.LimbA);
2395  }
2396  foreach (var limb in limbs)
2397  {
2398  TryUpdateSubParam(limb.Params, "spriteorientation".ToIdentifier(), float.NaN);
2399  if (limbPairEditing)
2400  {
2401  UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), float.NaN));
2402  }
2403  }
2404  return true;
2405  }
2406  };
2407  // Limb controls
2408  limbControls = new GUIFrame(new RectTransform(Vector2.One, centerArea.RectTransform), style: null) { CanBeFocused = false };
2409  var layoutGroupLimbControls = new GUILayoutGroup(new RectTransform(Vector2.One, limbControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false };
2410  lockSpriteOriginToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteOrigin"))
2411  {
2412  TextColor = Color.White,
2413  Selected = lockSpriteOrigin,
2414  OnSelected = (GUITickBox box) =>
2415  {
2416  lockSpriteOrigin = box.Selected;
2417  return true;
2418  }
2419  };
2420  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpritePosition"))
2421  {
2422  TextColor = Color.White,
2423  Selected = lockSpritePosition,
2424  OnSelected = (GUITickBox box) =>
2425  {
2426  lockSpritePosition = box.Selected;
2427  return true;
2428  }
2429  };
2430  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("LockSpriteSize"))
2431  {
2432  TextColor = Color.White,
2433  Selected = lockSpriteSize,
2434  OnSelected = (GUITickBox box) =>
2435  {
2436  lockSpriteSize = box.Selected;
2437  return true;
2438  }
2439  };
2440  recalculateColliderToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("AdjustCollider"))
2441  {
2442  TextColor = Color.White,
2443  Selected = recalculateCollider,
2444  OnSelected = (GUITickBox box) =>
2445  {
2446  recalculateCollider = box.Selected;
2447  showCollidersToggle.Selected = recalculateCollider;
2448  return true;
2449  }
2450  };
2451  new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupLimbControls.RectTransform), GetCharacterEditorTranslation("OnlyShowSelectedLimbs"))
2452  {
2453  TextColor = Color.White,
2454  Selected = onlyShowSourceRectForSelectedLimbs,
2455  OnSelected = (GUITickBox box) =>
2456  {
2457  onlyShowSourceRectForSelectedLimbs = box.Selected;
2458  return true;
2459  }
2460  };
2461 
2462  // Joint controls
2463  Point sliderSize = new Point(300, 20).Multiply(GUI.Scale);
2464  jointControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.075f), centerArea.RectTransform), style: null) { CanBeFocused = false };
2465  var layoutGroupJoints = new GUILayoutGroup(new RectTransform(Vector2.One, jointControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false };
2466  copyJointsToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupJoints.RectTransform), GetCharacterEditorTranslation("CopyJointSettings"))
2467  {
2468  ToolTip = GetCharacterEditorTranslation("CopyJointSettingsTooltip"),
2469  Selected = copyJointSettings,
2470  TextColor = copyJointSettings ? GUIStyle.Red : Color.White,
2471  OnSelected = (GUITickBox box) =>
2472  {
2473  copyJointSettings = box.Selected;
2474  box.TextColor = copyJointSettings ? GUIStyle.Red : Color.White;
2475  return true;
2476  }
2477  };
2478  // Ragdoll controls
2479  ragdollControls = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.25f), centerArea.RectTransform), style: null) { CanBeFocused = false };
2480  var layoutGroupRagdoll = new GUILayoutGroup(new RectTransform(Vector2.One, ragdollControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false };
2481  var uniformScalingToggle = new GUITickBox(new RectTransform(new Point(elementSize.X, textAreaHeight), layoutGroupRagdoll.RectTransform), GetCharacterEditorTranslation("UniformScale"))
2482  {
2483  Selected = uniformScaling,
2484  OnSelected = (GUITickBox box) =>
2485  {
2486  uniformScaling = box.Selected;
2487  return true;
2488  }
2489  };
2490  uniformScalingToggle.TextColor = Color.White;
2491  var jointScaleElement = new GUIFrame(new RectTransform(sliderSize + new Point(0, textAreaHeight), layoutGroupRagdoll.RectTransform), style: null);
2492  var jointScaleText = new GUITextBlock(new RectTransform(new Point(elementSize.X, textAreaHeight), jointScaleElement.RectTransform), $"{GetCharacterEditorTranslation("JointScale")}: {RagdollParams.JointScale.FormatDoubleDecimal()}", Color.WhiteSmoke, textAlignment: Alignment.Center);
2493  var limbScaleElement = new GUIFrame(new RectTransform(sliderSize + new Point(0, textAreaHeight), layoutGroupRagdoll.RectTransform), style: null);
2494  var limbScaleText = new GUITextBlock(new RectTransform(new Point(elementSize.X, textAreaHeight), limbScaleElement.RectTransform), $"{GetCharacterEditorTranslation("LimbScale")}: {RagdollParams.LimbScale.FormatDoubleDecimal()}", Color.WhiteSmoke, textAlignment: Alignment.Center);
2495  jointScaleBar = new GUIScrollBar(new RectTransform(sliderSize, jointScaleElement.RectTransform, Anchor.BottomLeft), barSize: 0.1f, style: "GUISlider")
2496  {
2497  BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE, RagdollParams.JointScale)),
2498  Step = 0.001f,
2499  OnMoved = (scrollBar, value) =>
2500  {
2501  float v = MathHelper.Lerp(RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE, value);
2502  UpdateJointScale(v);
2503  if (uniformScaling)
2504  {
2505  UpdateLimbScale(v);
2506  limbScaleBar.BarScroll = value;
2507  }
2508  return true;
2509  }
2510  };
2511  limbScaleBar = new GUIScrollBar(new RectTransform(sliderSize, limbScaleElement.RectTransform, Anchor.BottomLeft), barSize: 0.1f, style: "GUISlider")
2512  {
2513  BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE, RagdollParams.LimbScale)),
2514  Step = 0.001f,
2515  OnMoved = (scrollBar, value) =>
2516  {
2517  float v = MathHelper.Lerp(RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE, value);
2518  UpdateLimbScale(v);
2519  if (uniformScaling)
2520  {
2521  UpdateJointScale(v);
2522  jointScaleBar.BarScroll = value;
2523  }
2524  return true;
2525  }
2526  };
2527  void UpdateJointScale(float value)
2528  {
2529  freezeToggle.Selected = false;
2530  TryUpdateRagdollParam("jointscale", value);
2531  jointScaleText.Text = $"{GetCharacterEditorTranslation("JointScale")}: {RagdollParams.JointScale.FormatDoubleDecimal()}";
2532  character.AnimController.ResetJoints();
2533  }
2534  void UpdateLimbScale(float value)
2535  {
2536  TryUpdateRagdollParam("limbscale", value);
2537  limbScaleText.Text = $"{GetCharacterEditorTranslation("LimbScale")}: {RagdollParams.LimbScale.FormatDoubleDecimal()}";
2538  }
2539  // TODO: doesn't trigger if the mouse is released while the cursor is outside the button rect
2540  limbScaleBar.Bar.OnClicked += (button, data) =>
2541  {
2542  RecreateRagdoll();
2543  RagdollParams.StoreSnapshot();
2544  return true;
2545  };
2546  jointScaleBar.Bar.OnClicked += (button, data) =>
2547  {
2548  if (uniformScaling)
2549  {
2550  RecreateRagdoll();
2551  }
2552  RagdollParams.StoreSnapshot();
2553  return true;
2554  };
2555 
2556  // Just an approximation
2557  Point buttonSize = new Point(200, 40).Multiply(GUI.Scale);
2558  extraRagdollControls = new GUIFrame(new RectTransform(new Point(buttonSize.X, buttonSize.Y * 4), centerArea.RectTransform, Anchor.BottomRight)
2559  {
2560  AbsoluteOffset = new Point(30, 0).Multiply(GUI.Scale),
2561  MinSize = new Point(0, 120)
2562  }, style: null, color: Color.Black)
2563  {
2564  CanBeFocused = false
2565  };
2566  var paddedFrame = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, extraRagdollControls.RectTransform, Anchor.Center))
2567  {
2568  Stretch = true,
2569  AbsoluteSpacing = 5
2570  };
2571  var buttons = GUI.CreateButtons(4, new Vector2(1, 0.25f), paddedFrame.RectTransform, Anchor.TopCenter, style: "GUIButtonSmallFreeScale");
2572  deleteSelectedButton = buttons[0];
2573  deleteSelectedButton.Text = GetCharacterEditorTranslation("DeleteSelected");
2574  deleteSelectedButton.OnClicked = (button, data) =>
2575  {
2576  DeleteSelected();
2577  return true;
2578  };
2579  duplicateLimbButton = buttons[1];
2580  duplicateLimbButton.Text = GetCharacterEditorTranslation("DuplicateLimb");
2581  duplicateLimbButton.OnClicked = (button, data) =>
2582  {
2583  CopyLimb(selectedLimbs.FirstOrDefault());
2584  return true;
2585  };
2586  createJointButton = buttons[2];
2587  createJointButton.Text = GetCharacterEditorTranslation("CreateJoint");
2588  createJointButton.OnClicked = (button, data) =>
2589  {
2590  ToggleJointCreationMode();
2591  return true;
2592  };
2593  createLimbButton = buttons[3];
2594  createLimbButton.Text = GetCharacterEditorTranslation("CreateLimb");
2595  createLimbButton.OnClicked = (button, data) =>
2596  {
2597  ToggleLimbCreationMode();
2598  return true;
2599  };
2600  GUITextBlock.AutoScaleAndNormalize(buttons.Select(b => b.TextBlock));
2601 
2602  // Animation
2603  animationControls = new GUIFrame(new RectTransform(Vector2.One, centerArea.RectTransform), style: null) { CanBeFocused = false };
2604  var layoutGroupAnimation = new GUILayoutGroup(new RectTransform(Vector2.One, animationControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false };
2605  var animationSelectionElement = new GUIFrame(new RectTransform(new Point(elementSize.X * 2 - (int)(5 * GUI.xScale), elementSize.Y), layoutGroupAnimation.RectTransform), style: null);
2606  var animationSelectionText = new GUITextBlock(new RectTransform(new Point(elementSize.X, elementSize.Y), animationSelectionElement.RectTransform), GetCharacterEditorTranslation("SelectedAnimation"), Color.WhiteSmoke, textAlignment: Alignment.CenterRight);
2607  animSelection = new GUIDropDown(new RectTransform(new Point((int)(150 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.Center, Pivot.CenterLeft), elementCount: 5);
2608  if (character.AnimController.CanWalk)
2609  {
2610  animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk);
2611  animSelection.AddItem(AnimationType.Run.ToString(), AnimationType.Run);
2612  }
2613  animSelection.AddItem(AnimationType.SwimSlow.ToString(), AnimationType.SwimSlow);
2614  animSelection.AddItem(AnimationType.SwimFast.ToString(), AnimationType.SwimFast);
2615  if (character.AnimController.CanWalk && character.IsHumanoid)
2616  {
2617  animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch);
2618  }
2619  if (character.AnimController.ForceSelectAnimationType == AnimationType.NotDefined)
2620  {
2621  animSelection.SelectItem(character.AnimController.CanWalk ? AnimationType.Walk : AnimationType.SwimSlow);
2622  }
2623  else
2624  {
2625  animSelection.SelectItem(character.AnimController.ForceSelectAnimationType);
2626  }
2627  animSelection.OnSelected += (element, data) =>
2628  {
2629  AnimationType previousAnim = character.AnimController.ForceSelectAnimationType;
2630  character.AnimController.ForceSelectAnimationType = (AnimationType)data;
2631  switch (character.AnimController.ForceSelectAnimationType)
2632  {
2633  case AnimationType.Walk:
2634  case AnimationType.Run:
2635  case AnimationType.Crouch:
2636  character.AnimController.forceStanding = true;
2637  character.ForceRun = character.AnimController.ForceSelectAnimationType == AnimationType.Run;
2638  if (!wallCollisionsEnabled)
2639  {
2640  SetWallCollisions(true);
2641  }
2642  if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run && previousAnim != AnimationType.Crouch)
2643  {
2644  TeleportTo(spawnPosition);
2645  }
2646  break;
2647  case AnimationType.SwimSlow:
2648  character.AnimController.forceStanding = false;
2649  character.ForceRun = false;
2650  if (wallCollisionsEnabled)
2651  {
2652  SetWallCollisions(false);
2653  }
2654  break;
2655  case AnimationType.SwimFast:
2656  character.AnimController.forceStanding = false;
2657  character.ForceRun = true;
2658  if (wallCollisionsEnabled)
2659  {
2660  SetWallCollisions(false);
2661  }
2662  break;
2663  default:
2664  throw new NotImplementedException();
2665  }
2666  ResetParamsEditor();
2667  return true;
2668  };
2669  }
2670 
2671  private void CreateCharacterSelectionPanel()
2672  {
2673  characterSelectionPanel = new GUIFrame(new RectTransform(new Vector2(1, 0.2f), rightArea.RectTransform));
2674  var content = new GUILayoutGroup(new RectTransform(innerScale, characterSelectionPanel.RectTransform, Anchor.Center))
2675  {
2676  Stretch = true
2677  };
2678  // Character selection
2679  var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont);
2680  var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform)
2681  {
2682  RelativeOffset = new Vector2(0, 0.2f)
2683  }, elementCount: 8, style: null);
2684  characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue);
2685  foreach (CharacterPrefab prefab in CharacterPrefab.Prefabs.OrderByDescending(p => p.Identifier))
2686  {
2687  Identifier speciesName = prefab.Identifier;
2688  if (ShowCreature(prefab))
2689  {
2690  characterDropDown.AddItem(speciesName.Value.CapitaliseFirstInvariant(), speciesName).SetAsFirstChild();
2691  }
2692  else if (!CreatureMetrics.Encountered.Contains(speciesName))
2693  {
2694  // Using a matching placeholder string here ("hidden").
2695  var element = characterDropDown.AddItem(TextManager.Get("hiddensubmarines"), Identifier.Empty, textColor: Color.Gray * 0.75f);
2696  element.SetAsLastChild();
2697  element.Enabled = false;
2698  }
2699  }
2700  characterDropDown.SelectItem(currentCharacterIdentifier);
2701  characterDropDown.OnSelected = (component, data) =>
2702  {
2703  Identifier characterIdentifier = (Identifier)data;
2704  if (characterIdentifier.IsEmpty) { return true; }
2705  try
2706  {
2707  SpawnCharacter(characterIdentifier);
2708  }
2709  catch (Exception e)
2710  {
2711  HandleSpawnException(characterIdentifier, e);
2712  }
2713  return true;
2714  };
2715  if (currentCharacterIdentifier == CharacterPrefab.HumanSpeciesName)
2716  {
2717  var jobDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.15f), content.RectTransform)
2718  {
2719  RelativeOffset = new Vector2(0, 0.45f)
2720  }, elementCount: 8, style: null);
2721  jobDropDown.ListBox.Color = new Color(jobDropDown.ListBox.Color.R, jobDropDown.ListBox.Color.G, jobDropDown.ListBox.Color.B, byte.MaxValue);
2722  jobDropDown.AddItem("None");
2723  JobPrefab.Prefabs.Where(j => !j.HiddenJob).ForEach(j => jobDropDown.AddItem(j.Name, j.Identifier));
2724  jobDropDown.SelectItem(selectedJob);
2725  jobDropDown.OnSelected = (component, data) =>
2726  {
2727  Identifier newJob = data is Identifier jobIdentifier ? jobIdentifier : Identifier.Empty;
2728  if (newJob != selectedJob)
2729  {
2730  selectedJob = newJob;
2731  SpawnCharacter(currentCharacterIdentifier);
2732  }
2733  return true;
2734  };
2735  }
2736  var charButtons = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), parent: content.RectTransform, anchor: Anchor.BottomLeft), style: null);
2737  var prevCharacterButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), charButtons.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("PreviousCharacter"));
2738  prevCharacterButton.TextBlock.AutoScaleHorizontal = true;
2739  prevCharacterButton.OnClicked += (b, obj) =>
2740  {
2741  Identifier characterIdentifier = GetPreviousCharacterIdentifier();
2742  try
2743  {
2744  SpawnCharacter(characterIdentifier);
2745  }
2746  catch (Exception e)
2747  {
2748  HandleSpawnException(characterIdentifier, e);
2749  }
2750  return true;
2751  };
2752  var nextCharacterButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), charButtons.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("NextCharacter"));
2753  prevCharacterButton.TextBlock.AutoScaleHorizontal = true;
2754  nextCharacterButton.OnClicked += (b, obj) =>
2755  {
2756  Identifier characterIdentifier = GetNextCharacterIdentifier();
2757  try
2758  {
2759  SpawnCharacter(characterIdentifier);
2760  }
2761  catch (Exception e)
2762  {
2763  HandleSpawnException(characterIdentifier, e);
2764  }
2765  return true;
2766  };
2767  charButtons.RectTransform.MinSize = new Point(0, prevCharacterButton.RectTransform.MinSize.Y);
2768  characterPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), characterSelectionPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right);
2769  characterSelectionPanel.RectTransform.MinSize = new Point(0, (int)(content.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.2f));
2770 
2771  void HandleSpawnException(Identifier characterIdentifier, Exception e)
2772  {
2773  if (characterIdentifier != CharacterPrefab.HumanSpeciesName)
2774  {
2775  DebugConsole.ThrowError($"Failed to spawn the character \"{characterIdentifier}\".", e);
2776  SpawnCharacter(CharacterPrefab.HumanSpeciesName);
2777  }
2778  else
2779  {
2780  throw new Exception($"Failed to spawn the character \"{characterIdentifier}\".", innerException: e);
2781  }
2782  }
2783  }
2784 
2785  private void CreateFileEditPanel()
2786  {
2787  Vector2 buttonSize = new Vector2(1, 0.04f);
2788 
2789  fileEditPanel = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), rightArea.RectTransform));
2790  var layoutGroup = new GUILayoutGroup(new RectTransform(innerScale, fileEditPanel.RectTransform, Anchor.Center))
2791  {
2792  AbsoluteSpacing = 1,
2793  Stretch = true
2794  };
2795 
2796  new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("FileEditPanel"), font: GUIStyle.LargeFont);
2797 
2798  // Spacing
2799  new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false };
2800  var saveAllButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), TextManager.Get("editor.saveall"));
2801  saveAllButton.Color = GUIStyle.Green;
2802  saveAllButton.OnClicked += (button, userData) =>
2803  {
2804 #if !DEBUG
2805  if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile))
2806  {
2807  GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont);
2808  return false;
2809  }
2810 #endif
2811  ContentPath texturePath = ContentPath.FromRaw(character.Prefab.ContentPackage, RagdollParams.Texture);
2812  if (!character.IsHuman && (texturePath.IsNullOrWhiteSpace() || !File.Exists(texturePath.Value)))
2813  {
2814  DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}");
2815  return false;
2816  }
2817  else
2818  {
2819  character.Params.Save();
2820  GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5);
2821  character.AnimController.SaveRagdoll();
2822  GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5);
2823  AnimParams.ForEach(p => p.Save());
2824  }
2825  return true;
2826  };
2827 
2828  // Spacing
2829  new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false };
2830 
2831  Vector2 messageBoxRelSize = new Vector2(0.5f, 0.7f);
2832  var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll"));
2833  saveRagdollButton.OnClicked += (button, userData) =>
2834  {
2835  var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize);
2836  var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, (int)(30 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), RagdollParams.Name.RemoveWhitespace());
2837  box.Buttons[0].OnClicked += (b, d) =>
2838  {
2839  box.Close();
2840  return true;
2841  };
2842  box.Buttons[1].OnClicked += (b, d) =>
2843  {
2844 #if !DEBUG
2845  if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile))
2846  {
2847  GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont);
2848  box.Close();
2849  return false;
2850  }
2851 #endif
2852  character.AnimController.SaveRagdoll(inputField.Text);
2853  GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), Color.Green, font: GUIStyle.Font);
2854  RagdollParams.ClearCache();
2855  ResetParamsEditor();
2856  box.Close();
2857  return true;
2858  };
2859  return true;
2860  };
2861  var loadRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadRagdoll"));
2862  loadRagdollButton.OnClicked += (button, userData) =>
2863  {
2864  var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize);
2865  loadBox.Buttons[0].OnClicked += loadBox.Close;
2866  var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform, Anchor.TopCenter))
2867  {
2868  PlaySoundOnSelect = true,
2869  };
2870  var deleteButton = loadBox.Buttons[2];
2871  deleteButton.Enabled = false;
2872  void PopulateListBox()
2873  {
2874  try
2875  {
2876  var filePaths = Directory.GetFiles(RagdollParams.Folder);
2877  foreach (var path in filePaths)
2878  {
2879  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) },
2880  ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80))
2881  {
2882  UserData = path,
2883  ToolTip = path
2884  };
2885  }
2886  }
2887  catch (Exception e)
2888  {
2889  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", RagdollParams.Folder), e);
2890  }
2891  }
2892  PopulateListBox();
2893  // Handle file selection
2894  string selectedFile = null;
2895  listBox.OnSelected += (component, data) =>
2896  {
2897  selectedFile = data as string;
2898  // Don't allow to delete the ragdoll that is currently in use, nor the default file.
2899  var fileName = Path.GetFileNameWithoutExtension(selectedFile);
2900  deleteButton.Enabled = fileName != RagdollParams.Name && fileName != RagdollParams.GetDefaultFileName(character.SpeciesName);
2901  return true;
2902  };
2903  deleteButton.OnClicked += (btn, data) =>
2904  {
2905  if (selectedFile == null)
2906  {
2907  loadBox.Close();
2908  return false;
2909  }
2910  var msgBox = new GUIMessageBox(
2911  TextManager.Get("DeleteDialogLabel"),
2912  TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile),
2913  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
2914  msgBox.Buttons[0].OnClicked += (b, d) =>
2915  {
2916  try
2917  {
2918  File.Delete(selectedFile);
2919  GUI.AddMessage(GetCharacterEditorTranslation("RagdollDeletedFrom").Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font);
2920  }
2921  catch (Exception e)
2922  {
2923  DebugConsole.ThrowErrorLocalized(TextManager.Get("DeleteFileError").Replace("[file]", selectedFile), e);
2924  }
2925  msgBox.Close();
2926  listBox.ClearChildren();
2927  PopulateListBox();
2928  selectedFile = null;
2929  return true;
2930  };
2931  msgBox.Buttons[1].OnClicked += (b, d) =>
2932  {
2933  msgBox.Close();
2934  return true;
2935  };
2936  return true;
2937  };
2938  loadBox.Buttons[1].OnClicked += (btn, data) =>
2939  {
2940  string fileName = Path.GetFileNameWithoutExtension(selectedFile);
2941  Identifier baseSpecies = character.GetBaseCharacterSpeciesName();
2942  var ragdoll = character.IsHumanoid
2943  ? RagdollParams.GetRagdollParams<HumanRagdollParams>(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage) as RagdollParams
2944  : RagdollParams.GetRagdollParams<FishRagdollParams>(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage);
2945  ragdoll.Reset(true);
2946  GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font);
2947  RecreateRagdoll(ragdoll);
2948  CreateContextualControls();
2949  loadBox.Close();
2950  return true;
2951  };
2952  return true;
2953  };
2954  var saveAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveAnimation"));
2955  saveAnimationButton.OnClicked += (button, userData) =>
2956  {
2957  var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize);
2958  var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null);
2959  var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform, Anchor.CenterLeft) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: ");
2960  var inputField = new GUITextBox(new RectTransform(new Vector2(0.45f, 1), textArea.RectTransform, Anchor.CenterRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name);
2961  // Type filtering
2962  var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), box.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null);
2963  var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: ");
2964  var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4);
2965  foreach (object enumValue in Enum.GetValues(typeof(AnimationType)))
2966  {
2967  if (!(enumValue is AnimationType.NotDefined))
2968  {
2969  typeDropdown.AddItem(enumValue.ToString(), enumValue);
2970  }
2971  }
2972  AnimationType selectedType = character.AnimController.ForceSelectAnimationType;
2973  typeDropdown.OnSelected = (component, data) =>
2974  {
2975  selectedType = (AnimationType)data;
2976  inputField.Text = character.AnimController.GetAnimationParamsFromType(selectedType)?.Name.RemoveWhitespace();
2977  return true;
2978  };
2979  typeDropdown.SelectItem(selectedType);
2980  box.Buttons[0].OnClicked += (b, d) =>
2981  {
2982  box.Close();
2983  return true;
2984  };
2985  box.Buttons[1].OnClicked += (b, d) =>
2986  {
2987 #if !DEBUG
2988  if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile))
2989  {
2990  GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont);
2991  box.Close();
2992  return false;
2993  }
2994 #endif
2995  var animParams = character.AnimController.GetAnimationParamsFromType(selectedType);
2996  if (animParams == null) { return true; }
2997  string fileName = inputField.Text;
2998  animParams.Save(fileName);
2999  string newPath = animParams.Path.ToString();
3000  GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", selectedType.ToString()).Replace("[path]", newPath), Color.Green, font: GUIStyle.Font);
3001  AnimationParams.ClearCache();
3002  ResetParamsEditor();
3003  box.Close();
3004  return true;
3005  };
3006  return true;
3007  };
3008  var loadAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadAnimation"));
3009  loadAnimationButton.OnClicked += (button, userData) =>
3010  {
3011  var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize);
3012  loadBox.Buttons[0].OnClicked += loadBox.Close;
3013  var listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.6f), loadBox.Content.RectTransform))
3014  {
3015  PlaySoundOnSelect = true,
3016  };
3017  var deleteButton = loadBox.Buttons[2];
3018  deleteButton.Enabled = false;
3019  // Type filtering
3020  var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.1f), loadBox.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null);
3021  var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: ");
3022  var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4);
3023  foreach (object enumValue in Enum.GetValues(typeof(AnimationType)))
3024  {
3025  if (!(enumValue is AnimationType.NotDefined))
3026  {
3027  typeDropdown.AddItem(enumValue.ToString(), enumValue);
3028  }
3029  }
3030  AnimationType selectedType = character.AnimController.ForceSelectAnimationType;
3031  typeDropdown.OnSelected = (component, data) =>
3032  {
3033  selectedType = (AnimationType)data;
3034  PopulateListBox();
3035  return true;
3036  };
3037  typeDropdown.SelectItem(selectedType);
3038  void PopulateListBox()
3039  {
3040  try
3041  {
3042  listBox.ClearChildren();
3043  var filePaths = Directory.GetFiles(CurrentAnimation.Folder);
3044  foreach (var path in AnimationParams.FilterAndSortFiles(filePaths, selectedType))
3045  {
3046  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80))
3047  {
3048  UserData = path,
3049  ToolTip = path
3050  };
3051  }
3052  }
3053  catch (Exception e)
3054  {
3055  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("CouldntOpenDirectory").Replace("[folder]", CurrentAnimation.Folder), e);
3056  }
3057  }
3058  PopulateListBox();
3059  // Handle file selection
3060  string selectedFile = null;
3061  listBox.OnSelected += (component, data) =>
3062  {
3063  selectedFile = data as string;
3064  // Don't allow to delete the animation that is currently in use, nor the default file.
3065  string fileName = Path.GetFileNameWithoutExtension(selectedFile);
3066  deleteButton.Enabled = fileName != CurrentAnimation.Name && fileName != AnimationParams.GetDefaultFileName(character.SpeciesName, CurrentAnimation.AnimationType);
3067  return true;
3068  };
3069  deleteButton.OnClicked += (btn, data) =>
3070  {
3071  if (selectedFile == null)
3072  {
3073  loadBox.Close();
3074  return false;
3075  }
3076  var msgBox = new GUIMessageBox(
3077  TextManager.Get("DeleteDialogLabel"),
3078  TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile),
3079  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
3080  msgBox.Buttons[0].OnClicked += (b, d) =>
3081  {
3082  try
3083  {
3084  File.Delete(selectedFile);
3085  GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeDeleted").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font);
3086  }
3087  catch (Exception e)
3088  {
3089  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", selectedFile), e);
3090  }
3091  msgBox.Close();
3092  PopulateListBox();
3093  selectedFile = null;
3094  return true;
3095  };
3096  msgBox.Buttons[1].OnClicked += (b, d) =>
3097  {
3098  msgBox.Close();
3099  return true;
3100  };
3101  return true;
3102  };
3103  loadBox.Buttons[1].OnClicked += (btn, data) =>
3104  {
3105  if (character.AnimController.TryLoadAnimation(selectedType, Path.GetFileNameWithoutExtension(selectedFile), out AnimationParams animationParams, throwErrors: true))
3106  {
3107  animationParams.Reset(forceReload: true);
3108  GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", animationParams.FileNameWithoutExtension), Color.WhiteSmoke, font: GUIStyle.Font);
3109  }
3110  ResetParamsEditor();
3111  loadBox.Close();
3112  return true;
3113  };
3114  return true;
3115  };
3116 
3117  // Spacing
3118  new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false };
3119  var resetButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetButton"));
3120  resetButton.Color = GUIStyle.Red;
3121  resetButton.OnClicked += (button, userData) =>
3122  {
3123  CharacterParams.Reset(true);
3124  AnimParams.ForEach(p => p.Reset(true));
3125  character.AnimController.ResetRagdoll();
3126  RecreateRagdoll();
3127  jointCreationMode = JointCreationMode.None;
3128  isDrawingLimb = false;
3129  newLimbRect = Rectangle.Empty;
3130  jointStartLimb = null;
3131  CreateGUI();
3132  return true;
3133  };
3134 
3135  // Spacing
3136  new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false };
3137  new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("CreateNewCharacter"))
3138  {
3139  OnClicked = (button, data) =>
3140  {
3141  ResetView();
3142  Wizard.Instance.SelectTab(Wizard.Tab.Character);
3143  return true;
3144  }
3145  };
3146  new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("CopyCharacter"))
3147  {
3148  ToolTip = GetCharacterEditorTranslation("CopyCharacterToolTip"),
3149  OnClicked = (button, data) =>
3150  {
3151  ResetView();
3152  PrepareCharacterCopy();
3153  Wizard.Instance.SelectTab(Wizard.Tab.Character);
3154  return true;
3155  }
3156  };
3157 
3158  GUITextBlock.AutoScaleAndNormalize(layoutGroup.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock));
3159 
3160  fileEditToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), fileEditPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right);
3161 
3162  void ResetView()
3163  {
3164  characterInfoToggle.Selected = false;
3165  ragdollToggle.Selected = false;
3166  limbsToggle.Selected = false;
3167  animsToggle.Selected = false;
3168  spritesheetToggle.Selected = false;
3169  jointsToggle.Selected = false;
3170  paramsToggle.Selected = false;
3171  skeletonToggle.Selected = false;
3172  damageModifiersToggle.Selected = false;
3173  }
3174 
3175  fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f));
3176  }
3177  #endregion
3178 
3179  public void PrepareCharacterCopy()
3180  {
3183  AnimParams.ForEach(a => a.Serialize());
3185  }
3186 
3187 #region ToggleButtons
3188  private enum Direction
3189  {
3190  Left,
3191  Right
3192  }
3193 
3194  private class ToggleButton
3195  {
3196  public readonly Direction dir;
3197  public readonly GUIButton toggleButton;
3198 
3199  public float OpenState { get; private set; } = 1;
3200 
3201  private bool isHidden;
3202  public bool IsHidden
3203  {
3204  get { return isHidden; }
3205  set
3206  {
3207  isHidden = value;
3208  RefreshToggleButtonState();
3209  }
3210  }
3211 
3212  public ToggleButton(RectTransform rectT, Direction dir)
3213  {
3214  toggleButton = new GUIButton(rectT, style: "UIToggleButton")
3215  {
3216  OnClicked = (button, data) =>
3217  {
3218  IsHidden = !IsHidden;
3219  return true;
3220  }
3221  };
3222  this.dir = dir;
3223  RefreshToggleButtonState();
3224  }
3225 
3226  public void RefreshToggleButtonState()
3227  {
3228  foreach (GUIComponent child in toggleButton.Children)
3229  {
3230  switch (dir)
3231  {
3232  case Direction.Right:
3233  child.SpriteEffects = isHidden ? SpriteEffects.None : SpriteEffects.FlipHorizontally;
3234  break;
3235  case Direction.Left:
3236  child.SpriteEffects = isHidden ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
3237  break;
3238  }
3239  }
3240  }
3241 
3242  public void UpdateOpenState(float deltaTime, Vector2 hiddenPos, RectTransform panel)
3243  {
3244  panel.AbsoluteOffset = new Vector2(MathHelper.SmoothStep(hiddenPos.X, 0.0f, OpenState), panel.AbsoluteOffset.Y).ToPoint();
3245  OpenState = isHidden ? Math.Max(OpenState - deltaTime * 5, 0) : Math.Min(OpenState + deltaTime * 5, 1);
3246  }
3247  }
3248 
3249 #endregion
3250 
3251 #region Params
3252  private CharacterParams CharacterParams => character.Params;
3253  private List<AnimationParams> AnimParams => character.AnimController.AllAnimParams;
3254  private AnimationParams CurrentAnimation => character.AnimController.CurrentAnimationParams;
3255  private RagdollParams RagdollParams => character.AnimController.RagdollParams;
3256 
3257  private void ResetParamsEditor()
3258  {
3259  ParamsEditor.Instance.Clear();
3260  if (!editRagdoll && !editCharacterInfo && !editJoints && !editLimbs && !editAnimations)
3261  {
3262  paramsToggle.Selected = false;
3263  return;
3264  }
3265  if (editCharacterInfo)
3266  {
3267  var mainEditor = ParamsEditor.Instance;
3268  CharacterParams.AddToEditor(mainEditor, space: 10);
3269  var characterEditor = CharacterParams.SerializableEntityEditor;
3270  // Add some space after the title
3271  characterEditor.AddCustomContent(new GUIFrame(new RectTransform(new Point(characterEditor.Rect.Width, (int)(10 * GUI.yScale)), characterEditor.RectTransform), style: null) { CanBeFocused = false }, 1);
3272  if (CharacterParams.AI != null)
3273  {
3274  CreateAddButton(CharacterParams.AI.SerializableEntityEditor, () => CharacterParams.AI.TryAddEmptyTarget(out _), GetCharacterEditorTranslation("AddAITarget"));
3275  foreach (var target in CharacterParams.AI.Targets)
3276  {
3277  CreateCloseButton(target.SerializableEntityEditor, () => CharacterParams.AI.RemoveTarget(target), size: 0.8f);
3278  }
3279  }
3280  foreach (var emitter in CharacterParams.BloodEmitters)
3281  {
3282  CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveBloodEmitter(emitter));
3283  }
3284  foreach (var emitter in CharacterParams.GibEmitters)
3285  {
3286  CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveGibEmitter(emitter));
3287  }
3288  foreach (var emitter in CharacterParams.DamageEmitters)
3289  {
3290  CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveDamageEmitter(emitter));
3291  }
3292  foreach (var sound in CharacterParams.Sounds)
3293  {
3294  CreateCloseButton(sound.SerializableEntityEditor, () => CharacterParams.RemoveSound(sound));
3295  }
3296  foreach (var inventory in CharacterParams.Inventories)
3297  {
3298  var editor = inventory.SerializableEntityEditor;
3299  CreateCloseButton(editor, () => CharacterParams.RemoveInventory(inventory));
3300  foreach (var item in inventory.Items)
3301  {
3302  CreateCloseButton(item.SerializableEntityEditor, () => inventory.RemoveItem(item), size: 0.8f);
3303  }
3304  CreateAddButton(editor, () => inventory.AddItem(), GetCharacterEditorTranslation("AddInventoryItem"));
3305  }
3306  CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddBloodEmitter(), GetCharacterEditorTranslation("AddBloodEmitter"));
3307  CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddGibEmitter(), GetCharacterEditorTranslation("AddGibEmitter"));
3308  CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddDamageEmitter(), GetCharacterEditorTranslation("AddDamageEmitter"));
3309  CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddSound(), GetCharacterEditorTranslation("AddSound"));
3310  CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddInventory(), GetCharacterEditorTranslation("AddInventory"));
3311  }
3312  else if (editAnimations)
3313  {
3314  character.AnimController.CurrentAnimationParams?.AddToEditor(ParamsEditor.Instance, space: 10);
3315  }
3316  else
3317  {
3318  if (editRagdoll)
3319  {
3320  RagdollParams.AddToEditor(ParamsEditor.Instance, alsoChildren: false, space: 10);
3321  RagdollParams.Colliders.ForEach(c => c.AddToEditor(ParamsEditor.Instance, false, 10));
3322  }
3323  else if (editJoints)
3324  {
3325  if (selectedJoints.Any())
3326  {
3327  selectedJoints.ForEach(j => j.Params.AddToEditor(ParamsEditor.Instance, true, space: 10));
3328  }
3329  else
3330  {
3331  RagdollParams.Joints.ForEach(jp => jp.AddToEditor(ParamsEditor.Instance, false, space: 10));
3332  }
3333  }
3334  else if (editLimbs)
3335  {
3336  if (selectedLimbs.Any())
3337  {
3338  foreach (var limb in selectedLimbs)
3339  {
3340  var mainEditor = ParamsEditor.Instance;
3341  var limbEditor = limb.Params.SerializableEntityEditor;
3342  limb.Params.AddToEditor(mainEditor, true, space: 0);
3343  foreach (var damageModifier in limb.Params.DamageModifiers)
3344  {
3345  CreateCloseButton(damageModifier.SerializableEntityEditor, () => limb.Params.RemoveDamageModifier(damageModifier));
3346  }
3347  if (limb.Params.Sound == null)
3348  {
3349  CreateAddButtonAtLast(mainEditor, () => limb.Params.AddSound(), GetCharacterEditorTranslation("AddSound"));
3350  }
3351  else
3352  {
3353  CreateCloseButton(limb.Params.Sound.SerializableEntityEditor, () => limb.Params.RemoveSound());
3354  }
3355  if (limb.Params.LightSource == null)
3356  {
3357  CreateAddButtonAtLast(mainEditor, () => limb.Params.AddLight(), GetCharacterEditorTranslation("AddLightSource"));
3358  }
3359  else
3360  {
3361  CreateCloseButton(limb.Params.LightSource.SerializableEntityEditor, () => limb.Params.RemoveLight());
3362  }
3363  if (limb.Params.Attack == null)
3364  {
3365  CreateAddButtonAtLast(mainEditor, () => limb.Params.AddAttack(), GetCharacterEditorTranslation("AddAttack"));
3366  }
3367  else
3368  {
3369  var attackParams = limb.Params.Attack;
3370  foreach (var affliction in attackParams.Attack.Afflictions)
3371  {
3372  if (attackParams.AfflictionEditors.TryGetValue(affliction.Key, out SerializableEntityEditor afflictionEditor))
3373  {
3374  CreateCloseButton(afflictionEditor, () => attackParams.RemoveAffliction(affliction.Value), size: 0.8f);
3375  }
3376  }
3377  var attackEditor = attackParams.SerializableEntityEditor;
3378  CreateAddButton(attackEditor, () => attackParams.AddNewAffliction(), GetCharacterEditorTranslation("AddAffliction"));
3379  CreateCloseButton(attackEditor, () => limb.Params.RemoveAttack());
3380  var space = new GUIFrame(new RectTransform(new Point(attackEditor.RectTransform.Rect.Width, (int)(20 * GUI.yScale)), attackEditor.RectTransform), style: null, color: ParamsEditor.Color)
3381  {
3382  CanBeFocused = false
3383  };
3384  attackEditor.AddCustomContent(space, attackEditor.ContentCount);
3385  }
3386  CreateAddButtonAtLast(mainEditor, () => limb.Params.AddDamageModifier(), GetCharacterEditorTranslation("AddDamageModifier"));
3387  }
3388  }
3389  else
3390  {
3391  character.AnimController.Limbs.ForEach(l => l.Params.AddToEditor(ParamsEditor.Instance, false, space: 10));
3392  }
3393  }
3394  }
3395 
3396  void CreateCloseButton(SerializableEntityEditor editor, Action onButtonClicked, float size = 1)
3397  {
3398  if (editor == null) { return; }
3399  int height = 30;
3400  var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(height * size * GUI.yScale)), editor.RectTransform, isFixedSize: true), style: null)
3401  {
3402  CanBeFocused = false
3403  };
3404  new GUIButton(new RectTransform(new Vector2(0.9f), parent.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton", color: GUIStyle.Red)
3405  {
3406  OnClicked = (button, data) =>
3407  {
3408  onButtonClicked();
3409  ResetParamsEditor();
3410  return true;
3411  }
3412  };
3413  editor.AddCustomContent(parent, 0);
3414  }
3415 
3416  void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, LocalizedString text)
3417  {
3418  if (editor == null) { return; }
3419  var parentFrame = new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(50 * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color)
3420  {
3421  CanBeFocused = false
3422  };
3423  new GUIButton(new RectTransform(new Vector2(0.45f, 0.6f), parentFrame.RectTransform, Anchor.Center), text)
3424  {
3425  OnClicked = (button, data) =>
3426  {
3427  onButtonClicked();
3428  ResetParamsEditor();
3429  return true;
3430  }
3431  };
3432  }
3433 
3434  void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, LocalizedString text)
3435  {
3436  if (editor == null) { return; }
3437  var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(60 * GUI.yScale)), editor.RectTransform), style: null)
3438  {
3439  CanBeFocused = false
3440  };
3441  new GUIButton(new RectTransform(new Vector2(0.45f, 0.4f), parent.RectTransform, Anchor.CenterLeft), text)
3442  {
3443  OnClicked = (button, data) =>
3444  {
3445  onButtonClicked();
3446  ResetParamsEditor();
3447  return true;
3448  }
3449  };
3450  editor.AddCustomContent(parent, editor.ContentCount);
3451  }
3452  }
3453 
3454  private void TryUpdateAnimParam(string name, object value) => TryUpdateAnimParam(name.ToIdentifier(), value);
3455  private void TryUpdateAnimParam(Identifier name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value);
3456  private void TryUpdateRagdollParam(string name, object value) => TryUpdateRagdollParam(name.ToIdentifier(), value);
3457  private void TryUpdateRagdollParam(Identifier name, object value) => TryUpdateParam(RagdollParams, name, value);
3458 
3459  private void TryUpdateParam(EditableParams editableParams, Identifier name, object value)
3460  {
3461  if (editableParams.SerializableEntityEditor == null)
3462  {
3463  editableParams.AddToEditor(ParamsEditor.Instance);
3464  }
3465  if (editableParams.SerializableProperties.TryGetValue(name, out SerializableProperty p))
3466  {
3467  editableParams.SerializableEntityEditor.UpdateValue(p, value);
3468  }
3469  }
3470 
3471  private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateJointParam(joint, name.ToIdentifier(), value);
3472  private void TryUpdateJointParam(LimbJoint joint, Identifier name, object value) => TryUpdateSubParam(joint.Params, name, value);
3473  private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateLimbParam(limb, name.ToIdentifier(), value);
3474  private void TryUpdateLimbParam(Limb limb, Identifier name, object value) => TryUpdateSubParam(limb.Params, name, value);
3475 
3476  private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, Identifier name, object value)
3477  {
3478  if (ragdollSubParams.SerializableEntityEditor == null)
3479  {
3480  ragdollSubParams.AddToEditor(ParamsEditor.Instance);
3481  }
3482  if (ragdollSubParams.SerializableProperties.TryGetValue(name, out SerializableProperty p))
3483  {
3484  ragdollSubParams.SerializableEntityEditor.UpdateValue(p, value);
3485  }
3486  else
3487  {
3488  var subParams = ragdollSubParams.SubParams.Where(sp => sp.SerializableProperties.ContainsKey(name)).FirstOrDefault();
3489  if (subParams != null)
3490  {
3491  if (subParams.SerializableProperties.TryGetValue(name, out p))
3492  {
3493  if (subParams.SerializableEntityEditor == null)
3494  {
3495  subParams.AddToEditor(ParamsEditor.Instance);
3496  }
3497  subParams.SerializableEntityEditor.UpdateValue(p, value);
3498  }
3499  }
3500  else
3501  {
3502  DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name.Value));
3503  }
3504  }
3505  }
3506 #endregion
3507 
3508 #region Helpers
3509  private Vector2 ScreenToSim(float x, float y) => ScreenToSim(new Vector2(x, y));
3510  private Vector2 ScreenToSim(Vector2 p) => ConvertUnits.ToSimUnits(Cam.ScreenToWorld(p)) + Submarine.MainSub.SimPosition;
3511  private Vector2 SimToScreen(float x, float y) => SimToScreen(new Vector2(x, y));
3512  private Vector2 SimToScreen(Vector2 p) => Cam.WorldToScreen(ConvertUnits.ToDisplayUnits(p + Submarine.MainSub.SimPosition));
3513 
3514  private bool IsMatchingLimb(Limb limb1, Limb limb2, LimbJoint joint1, LimbJoint joint2) =>
3515  joint1.BodyA == limb1.body.FarseerBody && joint2.BodyA == limb2.body.FarseerBody ||
3516  joint1.BodyB == limb1.body.FarseerBody && joint2.BodyB == limb2.body.FarseerBody;
3517 
3518  private void ValidateJoint(LimbJoint limbJoint)
3519  {
3520  if (limbJoint.UpperLimit < limbJoint.LowerLimit)
3521  {
3522  if (limbJoint.LowerLimit > 0.0f)
3523  {
3524  limbJoint.LowerLimit -= MathHelper.TwoPi;
3525  }
3526  if (limbJoint.UpperLimit < 0.0f)
3527  {
3528  limbJoint.UpperLimit += MathHelper.TwoPi;
3529  }
3530  }
3531  limbJoint.LowerLimit = MathUtils.WrapAnglePi(limbJoint.LowerLimit);
3532  limbJoint.UpperLimit = MathUtils.WrapAnglePi(limbJoint.UpperLimit);
3533  }
3534 
3535  private Limb GetClosestLimbOnRagdoll(Vector2 targetPos, Func<Limb, bool> filter = null)
3536  {
3537  Limb closestLimb = null;
3538  float closestDistance = float.MaxValue;
3539  foreach (Limb l in character.AnimController.Limbs)
3540  {
3541  if (filter == null ? true : filter(l))
3542  {
3543  float distance = Vector2.DistanceSquared(SimToScreen(l.SimPosition), targetPos);
3544  if (distance < closestDistance)
3545  {
3546  closestLimb = l;
3547  closestDistance = distance;
3548  }
3549  }
3550  }
3551  return closestLimb;
3552  }
3553 
3554  private Limb GetClosestLimbOnSpritesheet(Vector2 targetPos, Func<Limb, bool> filter = null)
3555  {
3556  Limb closestLimb = null;
3557  float closestDistance = float.MaxValue;
3558  foreach (Limb l in character.AnimController.Limbs)
3559  {
3560  if (l == null) { continue; }
3561  if (filter == null ? true : filter(l))
3562  {
3563  float distance = Vector2.DistanceSquared(GetLimbSpritesheetRect(l).Center.ToVector2(), targetPos);
3564  if (distance < closestDistance)
3565  {
3566  closestLimb = l;
3567  closestDistance = distance;
3568  }
3569  }
3570  }
3571  return closestLimb;
3572  }
3573 
3574  private Rectangle GetLimbSpritesheetRect(Limb limb)
3575  {
3576  int offsetX = spriteSheetOffsetX;
3577  int offsetY = spriteSheetOffsetY;
3578  Rectangle rect = Rectangle.Empty;
3579  if (Textures != null)
3580  {
3581  for (int i = 0; i < Textures.Count; i++)
3582  {
3583  if (limb.ActiveSprite.FilePath != texturePaths[i])
3584  {
3585  offsetY += (int)(Textures[i].Height * spriteSheetZoom);
3586  }
3587  else
3588  {
3589  rect = limb.ActiveSprite.SourceRect;
3590  rect.Size = rect.MultiplySize(spriteSheetZoom);
3591  rect.Location = rect.Location.Multiply(spriteSheetZoom);
3592  rect.X += offsetX;
3593  rect.Y += offsetY;
3594  break;
3595  }
3596  }
3597  }
3598  return rect;
3599  }
3600 
3601  private void UpdateSourceRect(Limb limb, Rectangle newRect, bool resize)
3602  {
3603  Sprite activeSprite = limb.ActiveSprite;
3604  activeSprite.SourceRect = newRect;
3605  if (limb.DamagedSprite != null)
3606  {
3607  limb.DamagedSprite.SourceRect = activeSprite.SourceRect;
3608  }
3609  Vector2 colliderSize = new Vector2(ConvertUnits.ToSimUnits(newRect.Width), ConvertUnits.ToSimUnits(newRect.Height));
3610  if (resize)
3611  {
3612  if (recalculateCollider)
3613  {
3614  RecalculateCollider(limb, colliderSize);
3615  }
3616  }
3617  var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(activeSprite));
3618  var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb);
3619  if (!resize && originWidget != null)
3620  {
3621  Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom;
3622  RecalculateOrigin(limb, newOrigin);
3623  }
3624  else
3625  {
3626  RecalculateOrigin(limb);
3627  }
3628  TryUpdateLimbParam(limb, "sourcerect", newRect);
3629  if (limbPairEditing)
3630  {
3631  UpdateOtherLimbs(limb, otherLimb =>
3632  {
3633  otherLimb.ActiveSprite.SourceRect = newRect;
3634  if (otherLimb.DamagedSprite != null)
3635  {
3636  otherLimb.DamagedSprite.SourceRect = newRect;
3637  }
3638  if (resize)
3639  {
3640  if (recalculateCollider)
3641  {
3642  RecalculateCollider(otherLimb, colliderSize);
3643  }
3644  }
3645  if (!resize && originWidget != null)
3646  {
3647  Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom;
3648  RecalculateOrigin(otherLimb, newOrigin);
3649  }
3650  else
3651  {
3652  RecalculateOrigin(otherLimb);
3653  }
3654  TryUpdateLimbParam(otherLimb, "sourcerect", newRect);
3655  });
3656  };
3657  }
3658 
3659  private void CalculateSpritesheetZoom()
3660  {
3661  var texture = textures.OrderByDescending(t => t.Width).FirstOrDefault();
3662  if (texture == null)
3663  {
3664  spriteSheetZoom = 1;
3665  return;
3666  }
3667  float width = texture.Width;
3668  float height = textures.Sum(t => t.Height);
3669  float margin = 20;
3670  if (unrestrictSpritesheet)
3671  {
3672  spriteSheetMaxZoom = (GameMain.GraphicsWidth - spriteSheetOffsetX * 2 - margin - leftArea.Rect.Width) / width;
3673  }
3674  else
3675  {
3676  if (height > width)
3677  {
3678  spriteSheetMaxZoom = (centerArea.Rect.Bottom - spriteSheetOffsetY - margin) / height;
3679  }
3680  else
3681  {
3682  spriteSheetMaxZoom = (centerArea.Rect.Left - spriteSheetOffsetX - margin) / width;
3683  }
3684  }
3685  spriteSheetMinZoom = spriteSheetMinZoom > spriteSheetMaxZoom ? spriteSheetMaxZoom : 0.25f;
3686  spriteSheetZoom = MathHelper.Clamp(1, spriteSheetMinZoom, spriteSheetMaxZoom);
3687  }
3688 
3689  private void HandleLimbSelection(Limb limb)
3690  {
3691  if (!editLimbs)
3692  {
3693  SetToggle(limbsToggle, true);
3694  }
3695  if (!selectedLimbs.Contains(limb))
3696  {
3697  if (!Widget.EnableMultiSelect)
3698  {
3699  selectedLimbs.Clear();
3700  }
3701  selectedLimbs.Add(limb);
3702  ResetParamsEditor();
3703  //RagdollParams.StoreState();
3704  }
3705  else if (Widget.EnableMultiSelect)
3706  {
3707  selectedLimbs.Remove(limb);
3708  ResetParamsEditor();
3709  }
3710  }
3711 
3712  private void OpenDoors()
3713  {
3714  foreach (var item in Item.ItemList)
3715  {
3716  foreach (var component in item.Components)
3717  {
3718  if (component is Items.Components.Door door)
3719  {
3720  door.IsOpen = true;
3721  }
3722  }
3723  }
3724  }
3725 
3726  private void SaveSnapshot()
3727  {
3728  if (editJoints || editLimbs || editIK)
3729  {
3730  RagdollParams.StoreSnapshot();
3731  }
3732  if (editAnimations)
3733  {
3734  CurrentAnimation.StoreSnapshot();
3735  }
3736  }
3737 
3738  private void ToggleJointCreationMode()
3739  {
3740  switch (jointCreationMode)
3741  {
3742  case JointCreationMode.None:
3743  jointCreationMode = JointCreationMode.Select;
3744  SetToggle(spritesheetToggle, true);
3745  break;
3746  case JointCreationMode.Select:
3747  case JointCreationMode.Create:
3748  jointCreationMode = JointCreationMode.None;
3749  break;
3750  }
3751  }
3752 
3753  private void ToggleLimbCreationMode()
3754  {
3755  isDrawingLimb = !isDrawingLimb;
3756  if (isDrawingLimb)
3757  {
3758  SetToggle(spritesheetToggle, true);
3759  }
3760  }
3761 #endregion
3762 
3763 #region Animation Controls
3764  private void DrawAnimationControls(SpriteBatch spriteBatch, float deltaTime)
3765  {
3766  var collider = character.AnimController.Collider;
3767  var colliderDrawPos = cam.WorldToScreen(collider.DrawPosition);
3768  var animParams = character.AnimController.CurrentAnimationParams;
3769  var groundedParams = animParams as GroundedMovementParams;
3770  var humanParams = animParams as IHumanAnimation;
3771  var humanGroundedParams = animParams as HumanGroundedParams;
3772  var humanSwimParams = animParams as HumanSwimParams;
3773  var fishParams = animParams as IFishAnimation;
3774  var fishGroundedParams = animParams as FishGroundedParams;
3775  var fishSwimParams = animParams as FishSwimParams;
3776  var head = character.AnimController.GetLimb(LimbType.Head);
3777  var torso = character.AnimController.GetLimb(LimbType.Torso);
3778  var tail = character.AnimController.GetLimb(LimbType.Tail);
3779  var legs = character.AnimController.GetLimb(LimbType.Legs);
3780  var thigh = character.AnimController.GetLimb(LimbType.RightThigh) ?? character.AnimController.GetLimb(LimbType.LeftThigh);
3781  var foot = character.AnimController.GetLimb(LimbType.RightFoot) ?? character.AnimController.GetLimb(LimbType.LeftFoot);
3782  var hand = character.AnimController.GetLimb(LimbType.RightHand) ?? character.AnimController.GetLimb(LimbType.LeftHand);
3783  var arm = character.AnimController.GetLimb(LimbType.RightArm) ?? character.AnimController.GetLimb(LimbType.LeftArm);
3784  // Note: the main collider rotates only when swimming
3785  float dir = character.AnimController.Dir;
3786  Vector2 GetSimSpaceForward() => animParams.IsSwimAnimation ? Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)) : Vector2.UnitX * character.AnimController.Dir;
3787  Vector2 GetScreenSpaceForward() => animParams.IsSwimAnimation ? VectorExtensions.BackwardFlipped(collider.Rotation, 1) : Vector2.UnitX * character.AnimController.Dir;
3788  bool ShowCycleWidget() => PlayerInput.KeyDown(Keys.LeftAlt) && (CurrentAnimation is IHumanAnimation || CurrentAnimation is GroundedMovementParams);
3789  if (!PlayerInput.KeyDown(Keys.LeftAlt) && (animParams is IHumanAnimation || animParams is GroundedMovementParams))
3790  {
3791  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font);
3792  }
3793  // Widgets for all anims -->
3794  Vector2 referencePoint = cam.WorldToScreen(head != null ? head.DrawPosition: collider.DrawPosition);
3795  Vector2 drawPos = referencePoint;
3796  if (ShowCycleWidget())
3797  {
3798  GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w =>
3799  {
3800  float multiplier = 0.5f;
3801  w.Tooltip = GetCharacterEditorTranslation("CycleSpeed");
3802  w.Refresh = () =>
3803  {
3804  var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition);
3805  w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.CycleSpeed * multiplier) * Cam.Zoom;
3806  // Update tooltip, because the cycle speed might be automatically adjusted by the movement speed widget.
3807  w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}";
3808  };
3809  w.MouseHeld += dTime =>
3810  {
3811  // TODO: clamp so that cannot manipulate the local y axis -> remove the additional refresh callback in below
3812  //Vector2 newPos = PlayerInput.MousePosition;
3813  //w.DrawPos = newPos;
3814  float speed = CurrentAnimation.CycleSpeed + ConvertUnits.ToSimUnits(Vector2.Multiply(PlayerInput.MouseSpeed / multiplier, GetScreenSpaceForward()).Combine()) / Cam.Zoom;
3815  TryUpdateAnimParam("cyclespeed", speed);
3816  w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}";
3817  };
3818  // Additional check, which overrides the previous value (because evaluated last)
3819  w.PreUpdate += dTime =>
3820  {
3821  if (!ShowCycleWidget())
3822  {
3823  w.Enabled = false;
3824  }
3825  };
3826  // Additional (remove if the position is updated when the mouse is held)
3827  w.PreDraw += (sp, dTime) =>
3828  {
3829  if (w.IsControlled)
3830  {
3831  w.Refresh();
3832  }
3833  };
3834  w.PostDraw += (sp, dTime) =>
3835  {
3836  if (w.IsSelected)
3837  {
3838  GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.MediumPurple);
3839  }
3840  };
3841  }).Draw(spriteBatch, deltaTime);
3842  }
3843  else
3844  {
3845  GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w =>
3846  {
3847  float multiplier = 0.5f;
3848  w.Tooltip = GetCharacterEditorTranslation("MovementSpeed");
3849  w.Refresh = () =>
3850  {
3851  var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition);
3852  w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.MovementSpeed * multiplier) * Cam.Zoom;
3853  };
3854  w.MouseHeld += dTime =>
3855  {
3856  // TODO: clamp so that cannot manipulate the local y axis -> remove the additional refresh callback in below
3857  //Vector2 newPos = PlayerInput.MousePosition;
3858  //w.DrawPos = newPos;
3859  float speed = CurrentAnimation.MovementSpeed + ConvertUnits.ToSimUnits(Vector2.Multiply(PlayerInput.MouseSpeed / multiplier, GetScreenSpaceForward()).Combine()) / Cam.Zoom;
3860  TryUpdateAnimParam("movementspeed", MathHelper.Clamp(speed, 0.1f, Ragdoll.MAX_SPEED));
3861  // Sync
3862  if (humanSwimParams != null)
3863  {
3864  TryUpdateAnimParam("cyclespeed", character.AnimController.CurrentAnimationParams.MovementSpeed);
3865  }
3866  w.Tooltip = $"{GetCharacterEditorTranslation("MovementSpeed")}: {CurrentAnimation.MovementSpeed.FormatSingleDecimal()}";
3867  };
3868  // Additional check, which overrides the previous value (because evaluated last)
3869  w.PreUpdate += dTime =>
3870  {
3871  if (ShowCycleWidget())
3872  {
3873  w.Enabled = false;
3874  }
3875  };
3876  // Additional (remove if the position is updated when the mouse is held)
3877  w.PreDraw += (sp, dTime) =>
3878  {
3879  if (w.IsControlled)
3880  {
3881  w.Refresh();
3882  }
3883  };
3884  w.PostDraw += (sp, dTime) =>
3885  {
3886  if (w.IsSelected)
3887  {
3888  GUI.DrawLine(spriteBatch, w.DrawPos, Cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.Turquoise);
3889  }
3890  };
3891  }).Draw(spriteBatch, deltaTime);
3892  }
3893 
3894  if (head != null)
3895  {
3896  // Head angle
3897  DrawRadialWidget(spriteBatch, Cam.WorldToScreen(head.DrawPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White,
3898  angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true);
3899  // Head position and leaning
3900  Color color = GUIStyle.Red;
3901  if (animParams.IsGroundedAnimation)
3902  {
3903  if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController)
3904  {
3905  GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w =>
3906  {
3907  w.Tooltip = GetCharacterEditorTranslation("Head");
3908  w.Refresh = () => w.DrawPos = Cam.WorldToScreen(
3909  new Vector2(
3910  head.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.HeadLeanAmount * character.AnimController.Dir),
3911  ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y)));
3912  bool isHorizontal = false;
3913  bool isDirectionSet = false;
3914  w.MouseDown += () => isDirectionSet = false;
3915  w.MouseHeld += dTime =>
3916  {
3917  if (PlayerInput.MouseSpeed.NearlyEquals(Vector2.Zero)) { return; }
3918  if (!isDirectionSet)
3919  {
3920  isHorizontal = Math.Abs(PlayerInput.MouseSpeed.X) > Math.Abs(PlayerInput.MouseSpeed.Y);
3921  isDirectionSet = true;
3922  }
3923  var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom;
3924  if (PlayerInput.KeyDown(Keys.LeftAlt))
3925  {
3926  if (isHorizontal)
3927  {
3928  TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir);
3929  w.Refresh();
3930  w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y);
3931  }
3932  else
3933  {
3934  TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale);
3935  w.Refresh();
3936  w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y);
3937  }
3938  }
3939  else
3940  {
3941  TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir);
3942  w.Refresh();
3943  w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y);
3944  TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale);
3945  w.Refresh();
3946  w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y);
3947  }
3948  };
3949  w.PostDraw += (sB, dTime) =>
3950  {
3951  if (w.IsControlled && isDirectionSet)
3952  {
3953  if (PlayerInput.KeyDown(Keys.LeftAlt))
3954  {
3955  if (isHorizontal)
3956  {
3957  GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color);
3958  }
3959  else
3960  {
3961  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
3962  }
3963  }
3964  else
3965  {
3966  GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color);
3967  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
3968  }
3969  }
3970  else if (w.IsSelected)
3971  {
3972  GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head.DrawPosition), color);
3973  }
3974  };
3975  }).Draw(spriteBatch, deltaTime);
3976  }
3977  else if (groundedParams != null)
3978  {
3979  GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w =>
3980  {
3981  w.Tooltip = GetCharacterEditorTranslation("HeadPosition");
3982  w.Refresh = () => w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y)));
3983  w.MouseHeld += dTime =>
3984  {
3985  w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y)));
3986  var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom / RagdollParams.JointScale;
3987  TryUpdateAnimParam("headposition", groundedParams.HeadPosition - scaledInput.Y);
3988  };
3989  w.PostDraw += (sB, dTime) =>
3990  {
3991  if (w.IsControlled)
3992  {
3993  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
3994  }
3995  };
3996  }).Draw(spriteBatch, deltaTime);
3997  }
3998  }
3999  }
4000  if (torso != null)
4001  {
4002  referencePoint = torso.DrawPosition;
4003  if (animParams is HumanGroundedParams || animParams is HumanSwimParams)
4004  {
4005  var f = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation));
4006  referencePoint -= f * 25f;
4007  }
4008  // Torso angle
4009  DrawRadialWidget(spriteBatch, cam.WorldToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White,
4010  angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: -collider.Rotation + torso.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true);
4011  Color color = Color.DodgerBlue;
4012  if (animParams.IsGroundedAnimation)
4013  {
4014  // Torso position and leaning
4015  if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController)
4016  {
4017  GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w =>
4018  {
4019  w.Tooltip = GetCharacterEditorTranslation("Torso");
4020  w.Refresh = () => w.DrawPos = cam.WorldToScreen(
4021  new Vector2(torso.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.TorsoLeanAmount * character.AnimController.Dir),
4022  ConvertUnits.ToDisplayUnits(torso.PullJointWorldAnchorB.Y)));
4023  bool isHorizontal = false;
4024  bool isDirectionSet = false;
4025  w.MouseDown += () => isDirectionSet = false;
4026  w.MouseHeld += dTime =>
4027  {
4028  if (PlayerInput.MouseSpeed.NearlyEquals(Vector2.Zero)) { return; }
4029  if (!isDirectionSet)
4030  {
4031  isHorizontal = Math.Abs(PlayerInput.MouseSpeed.X) > Math.Abs(PlayerInput.MouseSpeed.Y);
4032  isDirectionSet = true;
4033  }
4034  var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom;
4035  if (PlayerInput.KeyDown(Keys.LeftAlt))
4036  {
4037  if (isHorizontal)
4038  {
4039  TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir);
4040  w.Refresh();
4041  w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y);
4042  }
4043  else
4044  {
4045  TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale);
4046  w.Refresh();
4047  w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y);
4048  }
4049  }
4050  else
4051  {
4052  TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir);
4053  w.Refresh();
4054  w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y);
4055  TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale);
4056  w.Refresh();
4057  w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y);
4058  }
4059  };
4060  w.PostDraw += (sB, dTime) =>
4061  {
4062  if (w.IsControlled && isDirectionSet)
4063  {
4064  if (PlayerInput.KeyDown(Keys.LeftAlt))
4065  {
4066  if (isHorizontal)
4067  {
4068  GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color);
4069  }
4070  else
4071  {
4072  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
4073  }
4074  }
4075  else
4076  {
4077  GUI.DrawLine(spriteBatch, new Vector2(0, w.DrawPos.Y), new Vector2(GameMain.GraphicsWidth, w.DrawPos.Y), color);
4078  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
4079  }
4080  }
4081  else if (w.IsSelected)
4082  {
4083  GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(torso.DrawPosition), color);
4084  }
4085  };
4086  }).Draw(spriteBatch, deltaTime);
4087  }
4088  else if (groundedParams != null)
4089  {
4090  GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w =>
4091  {
4092  w.Tooltip = GetCharacterEditorTranslation("TorsoPosition");
4093  w.Refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y);
4094  w.MouseHeld += dTime =>
4095  {
4096  w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y);
4097  var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom / RagdollParams.JointScale;
4098  TryUpdateAnimParam("torsoposition", groundedParams.TorsoPosition - scaledInput.Y);
4099  };
4100  w.PostDraw += (sB, dTime) =>
4101  {
4102  if (w.IsControlled)
4103  {
4104  GUI.DrawLine(spriteBatch, new Vector2(w.DrawPos.X, 0), new Vector2(w.DrawPos.X, GameMain.GraphicsHeight), color);
4105  }
4106  };
4107  }).Draw(spriteBatch, deltaTime);
4108  }
4109  }
4110  }
4111  // Tail angle
4112  if (tail != null && fishParams != null)
4113  {
4114  DrawRadialWidget(spriteBatch, cam.WorldToScreen(tail.DrawPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White,
4115  angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + tail.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true);
4116  }
4117  // Foot angle
4118  if (foot != null)
4119  {
4120  if (fishParams != null)
4121  {
4122  Vector2 colliderBottom = character.AnimController.GetColliderBottom();
4123  foreach (Limb limb in character.AnimController.Limbs)
4124  {
4125  if (limb.type != LimbType.LeftFoot && limb.type != LimbType.RightFoot) continue;
4126 
4127  if (!fishParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
4128  {
4129  fishParams.FootAnglesInRadians[limb.Params.ID] = 0.0f;
4130  }
4131 
4132  DrawRadialWidget(spriteBatch,
4133  cam.WorldToScreen(new Vector2(limb.DrawPosition.X, ConvertUnits.ToDisplayUnits(colliderBottom.Y))),
4134  MathHelper.ToDegrees(fishParams.FootAnglesInRadians[limb.Params.ID]),
4135  GetCharacterEditorTranslation("FootAngle"), Color.White,
4136  angle =>
4137  {
4138  fishParams.FootAnglesInRadians[limb.Params.ID] = MathHelper.ToRadians(angle);
4139  TryUpdateAnimParam("footangles", fishParams.FootAngles);
4140  },
4141  circleRadius: 25, rotationOffset: -collider.Rotation + limb.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true);
4142  }
4143  }
4144  else if (humanParams != null)
4145  {
4146  DrawRadialWidget(spriteBatch, cam.WorldToScreen(foot.DrawPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White,
4147  angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + foot.Params.GetSpriteOrientation() * dir, clockWise: dir > 0, wrapAnglePi: true);
4148  }
4149  // Grounded only
4150  if (groundedParams != null)
4151  {
4152  GetAnimationWidget("StepSize", Color.LimeGreen, Color.Black, initMethod: w =>
4153  {
4154  w.Tooltip = GetCharacterEditorTranslation("StepSize");
4155  w.Refresh = () =>
4156  {
4157  var refPoint = cam.WorldToScreen(new Vector2(
4158  character.AnimController.Collider.DrawPosition.X,
4159  character.AnimController.GetColliderBottom().Y));
4160  var stepSize = ConvertUnits.ToDisplayUnits(character.AnimController.StepSize.Value);
4161  w.DrawPos = refPoint + new Vector2(stepSize.X * character.AnimController.Dir, -stepSize.Y) * Cam.Zoom;
4162  };
4163  w.MouseHeld += dTime =>
4164  {
4165  w.DrawPos = PlayerInput.MousePosition;
4166  var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, -PlayerInput.MouseSpeed.Y)) / Cam.Zoom / RagdollParams.JointScale;
4167  TryUpdateAnimParam("stepsize", groundedParams.StepSize + transformedInput);
4168  w.Tooltip = $"{GetCharacterEditorTranslation("StepSize")}: {groundedParams.StepSize.FormatDoubleDecimal()}";
4169  };
4170  w.PostDraw += (sp, dTime) =>
4171  {
4172  if (w.IsSelected)
4173  {
4174  GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.GetColliderBottom()), Color.LimeGreen);
4175  }
4176  };
4177  }).Draw(spriteBatch, deltaTime);
4178  }
4179  }
4180  // Human grounded only -->
4181  if (humanGroundedParams != null)
4182  {
4183  if (hand != null || arm != null)
4184  {
4185  GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w =>
4186  {
4187  w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount");
4188  float offset = 10f;
4189  w.Refresh = () =>
4190  {
4191  var refPoint = cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset);
4192  var handMovement = ConvertUnits.ToDisplayUnits(humanGroundedParams.HandMoveAmount);
4193  w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom;
4194  };
4195  w.MouseHeld += dTime =>
4196  {
4197  w.DrawPos = PlayerInput.MousePosition;
4198  var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, PlayerInput.MouseSpeed.Y) / Cam.Zoom);
4199  TryUpdateAnimParam("handmoveamount", humanGroundedParams.HandMoveAmount + transformedInput);
4200  w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanGroundedParams.HandMoveAmount.FormatDoubleDecimal()}";
4201  };
4202  w.PostDraw += (sp, dTime) =>
4203  {
4204  if (w.IsSelected)
4205  {
4206  GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green);
4207  }
4208  };
4209  }).Draw(spriteBatch, deltaTime);
4210  }
4211  }
4212  // Fish swim only -->
4213  else if (tail != null && fishSwimParams != null)
4214  {
4215  float amplitudeMultiplier = 20;
4216  float lengthMultiplier = 20;
4217  int points = 1000;
4218  float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier;
4219  float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier;
4220  Vector2 GetRefPoint() => cam.WorldToScreen(collider.DrawPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom;
4221  Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength();
4222  Vector2 GetDir() => GetRefPoint() - GetDrawPos();
4223  Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2;
4224  Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude();
4225  var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w =>
4226  {
4227  w.Tooltip = GetCharacterEditorTranslation("TailMovementSpeed");
4228  w.Refresh = () => w.DrawPos = GetDrawPos();
4229  w.MouseHeld += dTime =>
4230  {
4231  float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier;
4232  TryUpdateAnimParam("wavelength", MathHelper.Clamp(fishSwimParams.WaveLength - input, 0, 200));
4233  };
4234  // Additional
4235  w.PreDraw += (sp, dTime) =>
4236  {
4237  if (w.IsControlled)
4238  {
4239  w.Refresh();
4240  }
4241  };
4242  });
4243  var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w =>
4244  {
4245  w.Tooltip = GetCharacterEditorTranslation("TailMovementAmount");
4246  w.Refresh = () => w.DrawPos = GetControlPoint();
4247  w.MouseHeld += dTime =>
4248  {
4249  float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier;
4250  TryUpdateAnimParam("waveamplitude", MathHelper.Clamp(fishSwimParams.WaveAmplitude + input, -100, 100));
4251  };
4252  // Additional
4253  w.PreDraw += (sp, dTime) =>
4254  {
4255  if (w.IsControlled)
4256  {
4257  w.Refresh();
4258  }
4259  };
4260  });
4261  if (lengthWidget.IsControlled || amplitudeWidget.IsControlled)
4262  {
4263  GUI.DrawSineWithDots(spriteBatch, GetRefPoint(), -GetDir(), GetAmplitude(), GetWaveLength(), 5000, points, Color.NavajoWhite);
4264  }
4265  lengthWidget.Draw(spriteBatch, deltaTime);
4266  amplitudeWidget.Draw(spriteBatch, deltaTime);
4267  }
4268  // Human swim only -->
4269  else if (humanSwimParams != null)
4270  {
4271  // Legs
4272  float amplitudeMultiplier = 5;
4273  float lengthMultiplier = 5;
4274  int points = 1000;
4275  float GetAmplitude() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegMoveAmount) * Cam.Zoom / amplitudeMultiplier;
4276  float GetWaveLength() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegCycleLength) * Cam.Zoom / lengthMultiplier;
4277  Vector2 GetRefPoint() => cam.WorldToScreen(character.DrawPosition - GetScreenSpaceForward().FlipY() * 75);
4278  Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength();
4279  Vector2 GetDir() => GetRefPoint() - GetDrawPos();
4280  Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2;
4281  Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude();
4282  var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w =>
4283  {
4284  w.Tooltip = GetCharacterEditorTranslation("LegMovementSpeed");
4285  w.Refresh = () => w.DrawPos = GetDrawPos();
4286  w.MouseHeld += dTime =>
4287  {
4288  float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier;
4289  TryUpdateAnimParam("legcyclelength", MathHelper.Clamp(humanSwimParams.LegCycleLength - input, 0, 20));
4290  };
4291  // Additional
4292  w.PreDraw += (sp, dTime) =>
4293  {
4294  if (w.IsControlled)
4295  {
4296  w.Refresh();
4297  }
4298  };
4299  });
4300  var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w =>
4301  {
4302  w.Tooltip = GetCharacterEditorTranslation("LegMovementAmount");
4303  w.Refresh = () => w.DrawPos = GetControlPoint();
4304  w.MouseHeld += dTime =>
4305  {
4306  float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier;
4307  TryUpdateAnimParam("legmoveamount", MathHelper.Clamp(humanSwimParams.LegMoveAmount + input, -2, 2));
4308  };
4309  // Additional
4310  w.PreDraw += (sp, dTime) =>
4311  {
4312  if (w.IsControlled)
4313  {
4314  w.Refresh();
4315  }
4316  };
4317  });
4318  if (lengthWidget.IsControlled || amplitudeWidget.IsControlled)
4319  {
4320  GUI.DrawSineWithDots(spriteBatch, GetRefPoint(), -GetDir(), GetAmplitude(), GetWaveLength(), 5000, points, Color.NavajoWhite);
4321  }
4322  lengthWidget.Draw(spriteBatch, deltaTime);
4323  amplitudeWidget.Draw(spriteBatch, deltaTime);
4324  // Arms
4325  GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w =>
4326  {
4327  w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount");
4328  float offset = 40f;
4329  w.Refresh = () =>
4330  {
4331  var refPoint = cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset);
4332  var handMovement = ConvertUnits.ToDisplayUnits(humanSwimParams.HandMoveAmount);
4333  w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom;
4334  };
4335  w.MouseHeld += dTime =>
4336  {
4337  w.DrawPos = PlayerInput.MousePosition;
4338  Vector2 transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, PlayerInput.MouseSpeed.Y)) / Cam.Zoom;
4339  Vector2 handMovement = humanSwimParams.HandMoveAmount + transformedInput;
4340  TryUpdateAnimParam("handmoveamount", handMovement);
4341  TryUpdateAnimParam("handcyclespeed", handMovement.X * 4);
4342  w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanSwimParams.HandMoveAmount.FormatDoubleDecimal()}";
4343  };
4344  w.PostDraw += (sp, dTime) =>
4345  {
4346  if (w.IsSelected)
4347  {
4348  GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green);
4349  }
4350  };
4351  }).Draw(spriteBatch, deltaTime);
4352  }
4353 
4354  foreach (Limb limb in character.AnimController.Limbs)
4355  {
4356  if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot)
4357  {
4358  GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugRefPos) - Vector2.One * 3, Vector2.One * 6, Color.White, isFilled: true);
4359  GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugTargetPos) - Vector2.One * 3, Vector2.One * 6, GUIStyle.Green, isFilled: true);
4360  }
4361  }
4362  }
4363 #endregion
4364 
4365 #region Ragdoll
4366  private Vector2[] corners = new Vector2[4];
4367  private Vector2[] GetLimbPhysicRect(Limb limb)
4368  {
4369  Vector2 size = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) * Cam.Zoom;
4370  Vector2 up = VectorExtensions.BackwardFlipped(limb.Rotation);
4371  Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition);
4372  corners = MathUtils.GetImaginaryRect(corners, up, limbScreenPos, size);
4373  return corners;
4374  }
4375 
4376  private void DrawLimbEditor(SpriteBatch spriteBatch)
4377  {
4378  float inputMultiplier = 0.5f;
4379  foreach (Limb limb in character.AnimController.Limbs)
4380  {
4381  if (limb == null || limb.ActiveSprite == null) { continue; }
4382  var origin = limb.ActiveSprite.Origin;
4383  var sourceRect = limb.ActiveSprite.SourceRect;
4384  Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition);
4385  bool isSelected = selectedLimbs.Contains(limb);
4386  corners = GetLimbPhysicRect(limb);
4387  if (isSelected && jointStartLimb != limb && jointEndLimb != limb)
4388  {
4389  GUI.DrawRectangle(spriteBatch, corners, Color.Yellow, thickness: 3);
4390  }
4391  if (GUI.MouseOn == null && Widget.SelectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition))
4392  {
4393  if (isSelected)
4394  {
4395  // Origin
4396  if (!lockSpriteOrigin && PlayerInput.PrimaryMouseButtonHeld())
4397  {
4398  Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(limb.Rotation));
4399  var input = -scaledMouseSpeed * inputMultiplier / Cam.Zoom / limb.Scale / limb.TextureScale;
4400  var sprite = limb.ActiveSprite;
4401  origin += input.TransformVector(forward);
4402  var max = new Vector2(sourceRect.Width, sourceRect.Height);
4403  sprite.Origin = origin.Clamp(Vector2.Zero, max);
4404  if (limb.DamagedSprite != null)
4405  {
4406  limb.DamagedSprite.Origin = sprite.Origin;
4407  }
4408  if (character.AnimController.IsFlipped)
4409  {
4410  origin.X = Math.Abs(origin.X - sourceRect.Width);
4411  }
4412  TryUpdateLimbParam(limb, "origin", limb.ActiveSprite.RelativeOrigin);
4413  if (limbPairEditing)
4414  {
4415  UpdateOtherLimbs(limb, otherLimb =>
4416  {
4417  otherLimb.ActiveSprite.Origin = sprite.Origin;
4418  if (otherLimb.DamagedSprite != null)
4419  {
4420  otherLimb.DamagedSprite.Origin = sprite.Origin;
4421  }
4422  TryUpdateLimbParam(otherLimb, "origin", otherLimb.ActiveSprite.RelativeOrigin);
4423  });
4424  }
4425  GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.ActiveSprite.RelativeOrigin.FormatDoubleDecimal(), Color.Yellow, Color.Black * 0.5f);
4426  }
4427  }
4428  else
4429  {
4430  GUI.DrawRectangle(spriteBatch, corners, Color.White);
4431  GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f);
4432  }
4433  }
4434  }
4435  }
4436 
4437  private void DrawRagdoll(SpriteBatch spriteBatch, float deltaTime)
4438  {
4439  bool altDown = PlayerInput.KeyDown(Keys.LeftAlt);
4440 
4441  if (!altDown && editJoints && selectedJoints.Any() && jointCreationMode == JointCreationMode.None)
4442  {
4443  GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font);
4444  }
4445 
4446  foreach (Limb limb in character.AnimController.Limbs)
4447  {
4448  if (editIK)
4449  {
4450  if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot || limb.type == LimbType.LeftHand || limb.type == LimbType.RightHand)
4451  {
4452  var pullJointWidgetSize = new Vector2(5, 5);
4453  Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA) + limb.body.DrawPositionOffset;
4454  GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUIStyle.Red, true);
4455  DrawWidget(spriteBatch, tformedPullPos, WidgetType.Rectangle, 8, Color.Cyan, $"IK ({limb.Name})", () =>
4456  {
4457  if (!selectedLimbs.Contains(limb))
4458  {
4459  selectedLimbs.Add(limb);
4460  ResetParamsEditor();
4461  }
4462  limb.PullJointWorldAnchorA = ScreenToSim(PlayerInput.MousePosition);
4463  TryUpdateLimbParam(limb, "pullpos", ConvertUnits.ToDisplayUnits(limb.PullJointLocalAnchorA / limb.Params.Scale / limb.Params.Ragdoll.LimbScale));
4464  GUI.DrawLine(spriteBatch, SimToScreen(limb.SimPosition), tformedPullPos, Color.MediumPurple);
4465  });
4466  }
4467  }
4468  foreach (var joint in character.AnimController.LimbJoints)
4469  {
4470  Vector2 jointPos = Vector2.Zero;
4471  Vector2 otherPos = Vector2.Zero;
4472  Vector2 anchorPosA = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA);
4473  Vector2 anchorPosB = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB);
4474  if (joint.BodyA == limb.body.FarseerBody)
4475  {
4476  jointPos = anchorPosA;
4477  otherPos = anchorPosB;
4478  }
4479  else if (joint.BodyB == limb.body.FarseerBody)
4480  {
4481  jointPos = anchorPosB;
4482  otherPos = anchorPosA;
4483  }
4484  else
4485  {
4486  continue;
4487  }
4488  Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition);
4489  var f = Vector2.Transform(jointPos, Matrix.CreateRotationZ(limb.Rotation));
4490  f.Y = -f.Y;
4491  Vector2 tformedJointPos = limbScreenPos + f * Cam.Zoom;
4492  if (drawSkeleton)
4493  {
4494  ShapeExtensions.DrawPoint(spriteBatch, limbScreenPos, Color.Black, size: 5);
4495  ShapeExtensions.DrawPoint(spriteBatch, limbScreenPos, Color.White, size: 1);
4496  GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Black, width: 3);
4497  GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.White, width: 1);
4498  }
4499  if (editJoints)
4500  {
4501  if (altDown && joint.BodyA == limb.body.FarseerBody)
4502  {
4503  continue;
4504  }
4505  if (!altDown && joint.BodyB == limb.body.FarseerBody)
4506  {
4507  continue;
4508  }
4509  var selectionWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget ragdoll", joint);
4510  selectionWidget.DrawPos = tformedJointPos;
4511  selectionWidget.Draw(spriteBatch, deltaTime);
4512  if (selectedJoints.Contains(joint))
4513  {
4514  if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None)
4515  {
4516  var otherBody = limb == joint.LimbA ? joint.LimbB : joint.LimbA;
4517  float rotation = -otherBody.Rotation + limb.Params.GetSpriteOrientation();
4518  if (character.AnimController.Dir < 0)
4519  {
4520  rotation -= MathHelper.Pi;
4521  }
4522  DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: rotation, holdPosition: true);
4523  }
4524  Limb referenceLimb = altDown ? joint.LimbB : joint.LimbA;
4525  // Is the direction inversed incorrectly?
4526  Vector2 to = tformedJointPos - VectorExtensions.ForwardFlipped(referenceLimb.Rotation - referenceLimb.Params.GetSpriteOrientation(), 150);
4527  GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.LightGray * 0.7f, width: 2);
4528  var dotSize = new Vector2(5, 5);
4529  var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint());
4530  //GUI.DrawRectangle(spriteBatch, tformedJointPos - dotSize / 2, dotSize, color, true);
4531  //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3);
4532  //GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow * 0.5f, width: 3);
4533  //GUI.DrawRectangle(spriteBatch, inputRect, GUIStyle.Red);
4534 
4535  string tooltip = $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}";
4536  GUI.DrawString(spriteBatch, tformedJointPos - new Vector2(1.2f, 0.5f) * GUIStyle.Font.MeasureString(tooltip), tooltip, Color.White, Color.Black * 0.5f);
4537  if (PlayerInput.PrimaryMouseButtonHeld())
4538  {
4539  if (!selectionWidget.IsControlled) { continue; }
4540  if (jointCreationMode != JointCreationMode.None) { continue; }
4541  if (autoFreeze)
4542  {
4543  isFrozen = true;
4544  }
4545  else
4546  {
4547  character.AnimController.Collider.PhysEnabled = false;
4548  }
4549  Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed) / Cam.Zoom;
4550  input.Y = -input.Y;
4551  input = input.TransformVector(VectorExtensions.ForwardFlipped(limb.Rotation));
4552  if (joint.BodyA == limb.body.FarseerBody)
4553  {
4554  joint.LocalAnchorA += input;
4555  Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale);
4556  TryUpdateJointParam(joint, "limb1anchor", transformedValue);
4557  // Snap all selected joints to the first selected
4558  if (copyJointSettings)
4559  {
4560  foreach (var j in selectedJoints)
4561  {
4562  j.LocalAnchorA = joint.LocalAnchorA;
4563  TryUpdateJointParam(j, "limb1anchor", transformedValue);
4564  }
4565  }
4566  }
4567  else if (joint.BodyB == limb.body.FarseerBody)
4568  {
4569  joint.LocalAnchorB += input;
4570  Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale);
4571  TryUpdateJointParam(joint, "limb2anchor", transformedValue);
4572  // Snap all selected joints to the first selected
4573  if (copyJointSettings)
4574  {
4575  foreach (var j in selectedJoints)
4576  {
4577  j.LocalAnchorB = joint.LocalAnchorB;
4578  TryUpdateJointParam(j, "limb2anchor", transformedValue);
4579  }
4580  }
4581  }
4582  // Edit the other joints
4583  if (limbPairEditing)
4584  {
4585  UpdateOtherJoints(limb, (otherLimb, otherJoint) =>
4586  {
4587  if (joint.BodyA == limb.body.FarseerBody && otherJoint.BodyA == otherLimb.body.FarseerBody)
4588  {
4589  otherJoint.LocalAnchorA = joint.LocalAnchorA;
4590  TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale));
4591  }
4592  else if (joint.BodyB == limb.body.FarseerBody && otherJoint.BodyB == otherLimb.body.FarseerBody)
4593  {
4594  otherJoint.LocalAnchorB = joint.LocalAnchorB;
4595  TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale));
4596  }
4597  });
4598  }
4599  }
4600  else
4601  {
4602  isFrozen = freezeToggle.Selected;
4603  character.AnimController.Collider.PhysEnabled = true;
4604  }
4605  }
4606  }
4607  }
4608  }
4609  }
4610 
4611  private void UpdateOtherLimbs(Limb limb, Action<Limb> updateAction)
4612  {
4613  // Edit the other limbs
4614  if (limbPairEditing)
4615  {
4616  string limbType = limb.type.ToString();
4617  bool isLeft = limbType.Contains("Left");
4618  bool isRight = limbType.Contains("Right");
4619  if (isLeft || isRight)
4620  {
4621  if (character.AnimController.HasMultipleLimbsOfSameType)
4622  {
4623  GetOtherLimbs(limb)?.ForEach(l => UpdateOtherLimbs(l));
4624  }
4625  else
4626  {
4627  Limb otherLimb = GetOtherLimb(limbType, isLeft);
4628  if (otherLimb != null)
4629  {
4630  UpdateOtherLimbs(otherLimb);
4631  }
4632  }
4633  void UpdateOtherLimbs(Limb otherLimb)
4634  {
4635  updateAction(otherLimb);
4636  }
4637  }
4638  }
4639  }
4640 
4641  private void UpdateOtherJoints(Limb limb, Action<Limb, LimbJoint> updateAction)
4642  {
4643  // Edit the other joints
4644  if (limbPairEditing)
4645  {
4646  string limbType = limb.type.ToString();
4647  bool isLeft = limbType.Contains("Left");
4648  bool isRight = limbType.Contains("Right");
4649  if (isLeft || isRight)
4650  {
4651  if (character.AnimController.HasMultipleLimbsOfSameType)
4652  {
4653  GetOtherLimbs(limb)?.ForEach(l => UpdateOtherJoints(l));
4654  }
4655  else
4656  {
4657  Limb otherLimb = GetOtherLimb(limbType, isLeft);
4658  if (otherLimb != null)
4659  {
4660  UpdateOtherJoints(otherLimb);
4661  }
4662  }
4663  void UpdateOtherJoints(Limb otherLimb)
4664  {
4665  foreach (var otherJoint in character.AnimController.LimbJoints)
4666  {
4667  updateAction(otherLimb, otherJoint);
4668  }
4669  }
4670  }
4671  }
4672  }
4673 
4674  private Limb GetOtherLimb(string limbType, bool isLeft)
4675  {
4676  string otherLimbType = isLeft ? limbType.Replace("Left", "Right") : limbType.Replace("Right", "Left");
4677  if (Enum.TryParse(otherLimbType, out LimbType type))
4678  {
4679  return character.AnimController.GetLimb(type);
4680  }
4681  return null;
4682  }
4683 
4684  // TODO: optimize?, this method creates carbage (not much, but it's used frequently)
4685  private IEnumerable<Limb> GetOtherLimbs(Limb limb)
4686  {
4687  var otherLimbs = character.AnimController.Limbs.Where(l => l.type == limb.type && l != limb);
4688  string limbType = limb.type.ToString();
4689  string otherLimbType = limbType.Contains("Left") ? limbType.Replace("Left", "Right") : limbType.Replace("Right", "Left");
4690  if (Enum.TryParse(otherLimbType, out LimbType type))
4691  {
4692  otherLimbs = otherLimbs.Union(character.AnimController.Limbs.Where(l => l.type == type));
4693  }
4694  return otherLimbs;
4695  }
4696 #endregion
4697 
4698 #region Spritesheet
4699  private List<Texture2D> textures;
4700  private List<Texture2D> Textures
4701  {
4702  get
4703  {
4704  if (textures == null)
4705  {
4706  CreateTextures();
4707  }
4708  return textures;
4709  }
4710  }
4711  private List<string> texturePaths;
4712  private void CreateTextures()
4713  {
4714  textures = new List<Texture2D>();
4715  texturePaths = new List<string>();
4716  foreach (Limb limb in character.AnimController.Limbs)
4717  {
4718  if (limb.ActiveSprite == null || texturePaths.Contains(limb.ActiveSprite.FilePath.Value)) { continue; }
4719  if (limb.ActiveSprite.Texture == null) { continue; }
4720  textures.Add(limb.ActiveSprite.Texture);
4721  texturePaths.Add(limb.ActiveSprite.FilePath.Value);
4722  }
4723  }
4724 
4725  private void DrawSpritesheetEditor(SpriteBatch spriteBatch, float deltaTime)
4726  {
4727  int offsetX = spriteSheetOffsetX;
4728  int offsetY = spriteSheetOffsetY;
4729  for (int i = 0; i < Textures.Count; i++)
4730  {
4731  var texture = Textures[i];
4732  if (!hideBodySheet)
4733  {
4734  spriteBatch.Draw(texture,
4735  position: new Vector2(offsetX, offsetY),
4736  rotation: 0,
4737  origin: Vector2.Zero,
4738  sourceRectangle: null,
4739  scale: spriteSheetZoom,
4740  effects: SpriteEffects.None,
4741  color: Color.White,
4742  layerDepth: 0);
4743  }
4744  GUI.DrawRectangle(spriteBatch, new Vector2(offsetX, offsetY), texture.Bounds.Size.ToVector2() * spriteSheetZoom, Color.White);
4745  foreach (Limb limb in character.AnimController.Limbs)
4746  {
4747  if (limb.ActiveSprite == null || limb.ActiveSprite.FilePath != texturePaths[i]) { continue; }
4748  Rectangle rect = limb.ActiveSprite.SourceRect;
4749  rect.Size = rect.MultiplySize(spriteSheetZoom);
4750  rect.Location = rect.Location.Multiply(spriteSheetZoom);
4751  rect.X += offsetX;
4752  rect.Y += offsetY;
4753  Vector2 origin = limb.ActiveSprite.Origin;
4754  Vector2 limbScreenPos = new Vector2(rect.X + origin.X * spriteSheetZoom, rect.Y + origin.Y * spriteSheetZoom);
4755  // Draw the clothes
4756  foreach (var wearable in limb.WearingItems)
4757  {
4758  Vector2 orig = limb.ActiveSprite.Origin;
4759  if (!wearable.InheritOrigin)
4760  {
4761  orig = wearable.Sprite.Origin;
4762  // If the wearable inherits the origin, flipping is already handled.
4763  if (limb.body.Dir == -1.0f)
4764  {
4765  orig.X = wearable.Sprite.SourceRect.Width - orig.X;
4766  }
4767  }
4768  spriteBatch.Draw(wearable.Sprite.Texture,
4769  position: limbScreenPos,
4770  rotation: 0,
4771  origin: orig,
4772  sourceRectangle: wearable.InheritSourceRect ? limb.ActiveSprite.SourceRect : wearable.Sprite.SourceRect,
4773  scale: (wearable.InheritScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom,
4774  effects: SpriteEffects.None,
4775  color: Color.White,
4776  layerDepth: 0);
4777  }
4778  // The origin is manipulated when the character is flipped. We have to undo it here.
4779  if (character.AnimController.Dir < 0)
4780  {
4781  limbScreenPos.X = rect.X + rect.Width - (float)Math.Round(origin.X * spriteSheetZoom);
4782  }
4783  if (editJoints)
4784  {
4785  DrawSpritesheetJointEditor(spriteBatch, deltaTime, limb, limbScreenPos);
4786  }
4787  bool isMouseOn = rect.Contains(PlayerInput.MousePosition);
4788  if (editLimbs)
4789  {
4790  int widgetSize = 8;
4791  int halfSize = widgetSize / 2;
4792  Vector2 stringOffset = new Vector2(5, 14);
4793  var topLeft = rect.Location.ToVector2();
4794  var topRight = new Vector2(topLeft.X + rect.Width, topLeft.Y);
4795  var bottomRight = new Vector2(topRight.X, topRight.Y + rect.Height);
4796  bool isSelected = selectedLimbs.Contains(limb);
4797  if (jointStartLimb != limb && jointEndLimb != limb)
4798  {
4799  if (isSelected || !onlyShowSourceRectForSelectedLimbs)
4800  {
4801  GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : GUIStyle.Red));
4802  }
4803  }
4804  if (isSelected)
4805  {
4806  var sprite = limb.ActiveSprite;
4807  Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2();
4808  Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y);
4809  Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height);
4810  var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, WidgetShape.Cross, initMethod: w =>
4811  {
4812  w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}";
4813  w.Refresh();
4814  w.MouseHeld += dTime =>
4815  {
4816  var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite));
4817  w.DrawPos = PlayerInput.MousePosition.Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom);
4818  sprite.Origin = (w.DrawPos - spritePos - sprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom;
4819  if (limb.DamagedSprite != null)
4820  {
4821  limb.DamagedSprite.RelativeOrigin = sprite.RelativeOrigin;
4822  }
4823  TryUpdateLimbParam(limb, "origin", sprite.RelativeOrigin);
4824  if (limbPairEditing)
4825  {
4826  UpdateOtherLimbs(limb, otherLimb =>
4827  {
4828  otherLimb.ActiveSprite.RelativeOrigin = sprite.RelativeOrigin;
4829  if (otherLimb.DamagedSprite != null)
4830  {
4831  otherLimb.DamagedSprite.RelativeOrigin = sprite.RelativeOrigin;
4832  }
4833  TryUpdateLimbParam(otherLimb, "origin", sprite.RelativeOrigin);
4834  });
4835  }
4836  };
4837  w.PreUpdate += dTime =>
4838  {
4839  // Additional condition
4840  if (w.Enabled)
4841  {
4842  w.Enabled = !lockSpriteOrigin;
4843  }
4844  };
4845  w.PreDraw += (sb, dTime) =>
4846  {
4847  var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite));
4848  w.DrawPos = (spritePos + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * spriteSheetZoom)
4849  .Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom);
4850  w.Refresh();
4851  };
4852  });
4853  originWidget.Draw(spriteBatch, deltaTime);
4854  if (!lockSpritePosition && (limb.type != LimbType.Head || !character.IsHuman))
4855  {
4856  var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, WidgetShape.Rectangle, initMethod: w =>
4857  {
4858  w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}";
4859  w.Refresh();
4860  w.MouseHeld += dTime =>
4861  {
4862  w.DrawPos = PlayerInput.MousePosition;
4863  Sprite activeSprite = limb.ActiveSprite;
4864  var newRect = activeSprite.SourceRect;
4865  newRect.Location = new Point(
4866  (int)((PlayerInput.MousePosition.X + halfSize - spriteSheetOffsetX) / spriteSheetZoom),
4867  (int)((PlayerInput.MousePosition.Y + halfSize - GetOffsetY(activeSprite)) / spriteSheetZoom));
4868  activeSprite.SourceRect = newRect;
4869  if (limb.DamagedSprite != null)
4870  {
4871  limb.DamagedSprite.SourceRect = activeSprite.SourceRect;
4872  }
4873  TryUpdateLimbParam(limb, "sourcerect", newRect);
4874  var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(activeSprite));
4875  Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom;
4876  RecalculateOrigin(limb, newOrigin);
4877  if (limbPairEditing)
4878  {
4879  UpdateOtherLimbs(limb, otherLimb =>
4880  {
4881  otherLimb.ActiveSprite.SourceRect = newRect;
4882  if (otherLimb.DamagedSprite != null)
4883  {
4884  otherLimb.DamagedSprite.SourceRect = newRect;
4885  }
4886  TryUpdateLimbParam(otherLimb, "sourcerect", newRect);
4887  RecalculateOrigin(otherLimb, newOrigin);
4888  });
4889  };
4890  };
4891  w.PreDraw += (sb, dTime) => w.Refresh();
4892  });
4893  if (!positionWidget.IsControlled)
4894  {
4895  positionWidget.DrawPos = topLeft - new Vector2(halfSize);
4896  }
4897  positionWidget.Draw(spriteBatch, deltaTime);
4898  }
4899  if (!lockSpriteSize && (limb.type != LimbType.Head || !character.IsHuman))
4900  {
4901  var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, WidgetShape.Rectangle, initMethod: w =>
4902  {
4903  w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}";
4904  w.Refresh();
4905  w.MouseHeld += dTime =>
4906  {
4907  w.DrawPos = PlayerInput.MousePosition;
4908  Sprite activeSprite = limb.ActiveSprite;
4909  Rectangle newRect = activeSprite.SourceRect;
4910  float offset_y = activeSprite.SourceRect.Y * spriteSheetZoom + GetOffsetY(activeSprite);
4911  float offset_x = activeSprite.SourceRect.X * spriteSheetZoom + spriteSheetOffsetX;
4912  int width = (int)((PlayerInput.MousePosition.X - halfSize - offset_x) / spriteSheetZoom);
4913  int height = (int)((PlayerInput.MousePosition.Y - halfSize - offset_y) / spriteSheetZoom);
4914  newRect.Size = new Point(width, height);
4915  activeSprite.SourceRect = newRect;
4916  activeSprite.size = new Vector2(width, height);
4917  Vector2 colliderSize = new Vector2(ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height));
4918  if (recalculateCollider)
4919  {
4920  RecalculateCollider(limb, colliderSize);
4921  }
4922  RecalculateOrigin(limb);
4923  if (limb.DamagedSprite != null)
4924  {
4925  limb.DamagedSprite.SourceRect = activeSprite.SourceRect;
4926  }
4927  TryUpdateLimbParam(limb, "sourcerect", newRect);
4928  if (limbPairEditing)
4929  {
4930  UpdateOtherLimbs(limb, otherLimb =>
4931  {
4932  otherLimb.ActiveSprite.SourceRect = newRect;
4933  RecalculateOrigin(otherLimb);
4934  if (recalculateCollider)
4935  {
4936  RecalculateCollider(otherLimb, colliderSize);
4937  }
4938  if (otherLimb.DamagedSprite != null)
4939  {
4940  otherLimb.DamagedSprite.SourceRect = newRect;
4941  }
4942  TryUpdateLimbParam(otherLimb, "sourcerect", newRect);
4943  });
4944  };
4945  };
4946  w.PreDraw += (sb, dTime) => w.Refresh();
4947  });
4948  if (!sizeWidget.IsControlled)
4949  {
4950  sizeWidget.DrawPos = bottomRight + new Vector2(halfSize);
4951  }
4952  sizeWidget.Draw(spriteBatch, deltaTime);
4953  }
4954  }
4955  else if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None())
4956  {
4957  // TODO: only one limb name should be displayed (needs to be done in a separate loop)
4958  GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f);
4959  }
4960  }
4961  else
4962  {
4963  GUI.DrawRectangle(spriteBatch, rect, isMouseOn ? Color.White : Color.Gray);
4964  if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None())
4965  {
4966  // TODO: only one limb name should be displayed (needs to be done in a separate loop)
4967  GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f);
4968  }
4969  }
4970  }
4971  offsetY += (int)(texture.Height * spriteSheetZoom);
4972  }
4973  }
4974 
4975  private int GetTextureHeight(Sprite sprite)
4976  {
4977  int textureIndex = Textures.IndexOf(sprite.Texture);
4978  int height = 0;
4979  foreach (var t in Textures)
4980  {
4981  if (Textures.IndexOf(t) < textureIndex)
4982  {
4983  height += t.Height;
4984  }
4985  }
4986  return (int)(height * spriteSheetZoom);
4987  }
4988 
4989  private int GetOffsetY(Sprite sprite) => spriteSheetOffsetY + GetTextureHeight(sprite);
4990 
4991  private void RecalculateCollider(Limb l, Vector2 size)
4992  {
4993  // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic.
4994  float multiplier = 0.9f;
4995  l.body.SetSize(new Vector2(size.X, size.Y) * l.Scale * RagdollParams.TextureScale * multiplier);
4996  TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.Radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale));
4997  TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.Width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale));
4998  TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.Height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale));
4999  }
5000 
5001  private void RecalculateOrigin(Limb l, Vector2? newOrigin = null)
5002  {
5003  Sprite activeSprite = l.ActiveSprite;
5004  if (lockSpriteOrigin)
5005  {
5006  // Keeps the absolute origin unchanged. The relative origin will be recalculated.
5007  activeSprite.Origin = newOrigin ?? activeSprite.Origin;
5008  TryUpdateLimbParam(l, "origin", activeSprite.RelativeOrigin);
5009  }
5010  else
5011  {
5012  // Keeps the relative origin unchanged. The absolute origin will be recalculated.
5013  activeSprite.RelativeOrigin = activeSprite.RelativeOrigin;
5014  }
5015  }
5016 
5017  private void DrawSpritesheetJointEditor(SpriteBatch spriteBatch, float deltaTime, Limb limb, Vector2 limbScreenPos, float spriteRotation = 0)
5018  {
5019  foreach (var joint in character.AnimController.LimbJoints)
5020  {
5021  Vector2 jointPos = Vector2.Zero;
5022  Vector2 anchorPosA = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA);
5023  Vector2 anchorPosB = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB);
5024  string anchorID;
5025  string otherID;
5026  if (joint.BodyA == limb.body.FarseerBody)
5027  {
5028  jointPos = anchorPosA;
5029  anchorID = "1";
5030  otherID = "2";
5031  }
5032  else if (joint.BodyB == limb.body.FarseerBody)
5033  {
5034  jointPos = anchorPosB;
5035  anchorID = "2";
5036  otherID = "1";
5037  }
5038  else
5039  {
5040  continue;
5041  }
5042  Vector2 tformedJointPos = jointPos = jointPos / joint.Scale / limb.TextureScale * spriteSheetZoom;
5043  tformedJointPos.Y = -tformedJointPos.Y;
5044  tformedJointPos.X *= character.AnimController.Dir;
5045  tformedJointPos += limbScreenPos;
5046  var jointSelectionWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget {anchorID}", joint, $"{joint.Params.Name} selection widget {otherID}");
5047  jointSelectionWidget.DrawPos = tformedJointPos;
5048  jointSelectionWidget.Draw(spriteBatch, deltaTime);
5049  var otherWidget = GetJointSelectionWidget($"{joint.Params.Name} selection widget {otherID}", joint, $"{joint.Params.Name} selection widget {anchorID}");
5050  if (anchorID == "2")
5051  {
5052  bool isSelected = selectedJoints.Contains(joint);
5053  bool isHovered = jointSelectionWidget.IsSelected || otherWidget.IsSelected;
5054  if (isSelected || isHovered)
5055  {
5056  GUI.DrawLine(spriteBatch, jointSelectionWidget.DrawPos, otherWidget.DrawPos, jointSelectionWidget.Color, width: 2);
5057  }
5058  }
5059  if (selectedJoints.Contains(joint))
5060  {
5061  if (joint.LimitEnabled && jointCreationMode == JointCreationMode.None)
5062  {
5063  DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: false, allowPairEditing: true, holdPosition: false, rotationOffset: joint.LimbB.Params.GetSpriteOrientation());
5064  }
5065  if (jointSelectionWidget.IsControlled)
5066  {
5067  Vector2 input = ConvertUnits.ToSimUnits(scaledMouseSpeed);
5068  input.Y = -input.Y;
5069  input.X *= character.AnimController.Dir;
5070  input *= joint.Scale * limb.TextureScale / spriteSheetZoom;
5071  if (joint.BodyA == limb.body.FarseerBody)
5072  {
5073  joint.LocalAnchorA += input;
5074  Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale);
5075  TryUpdateJointParam(joint, "limb1anchor", transformedValue);
5076  // Snap all selected joints to the first selected
5077  if (copyJointSettings)
5078  {
5079  foreach (var j in selectedJoints)
5080  {
5081  j.LocalAnchorA = joint.LocalAnchorA;
5082  TryUpdateJointParam(j, "limb1anchor", transformedValue);
5083  }
5084  }
5085  }
5086  else if (joint.BodyB == limb.body.FarseerBody)
5087  {
5088  joint.LocalAnchorB += input;
5089  Vector2 transformedValue = ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale);
5090  TryUpdateJointParam(joint, "limb2anchor", transformedValue);
5091  // Snap all selected joints to the first selected
5092  if (copyJointSettings)
5093  {
5094  foreach (var j in selectedJoints)
5095  {
5096  j.LocalAnchorB = joint.LocalAnchorB;
5097  TryUpdateJointParam(j, "limb2anchor", transformedValue);
5098  }
5099  }
5100  }
5101  if (limbPairEditing)
5102  {
5103  UpdateOtherJoints(limb, (otherLimb, otherJoint) =>
5104  {
5105  if (joint.BodyA == limb.body.FarseerBody && otherJoint.BodyA == otherLimb.body.FarseerBody)
5106  {
5107  otherJoint.LocalAnchorA = joint.LocalAnchorA;
5108  TryUpdateJointParam(otherJoint, "limb1anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorA / joint.Scale));
5109  }
5110  else if (joint.BodyB == limb.body.FarseerBody && otherJoint.BodyB == otherLimb.body.FarseerBody)
5111  {
5112  otherJoint.LocalAnchorB = joint.LocalAnchorB;
5113  TryUpdateJointParam(otherJoint, "limb2anchor", ConvertUnits.ToDisplayUnits(joint.LocalAnchorB / joint.Scale));
5114  }
5115  });
5116  }
5117  }
5118  }
5119  }
5120  }
5121 
5122  private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0)
5123  {
5124  bool clockWise = joint.Params.ClockWiseRotation;
5125  Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUIStyle.Green * 0.5f : GUIStyle.Red;
5126  DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle =>
5127  {
5128  joint.UpperLimit = MathHelper.ToRadians(angle);
5129  ValidateJoint(joint);
5130  angle = MathHelper.ToDegrees(joint.UpperLimit);
5131  TryUpdateJointParam(joint, "upperlimit", angle);
5132  if (copyJointSettings)
5133  {
5134  foreach (var j in selectedJoints)
5135  {
5136  if (j.LimitEnabled != joint.LimitEnabled)
5137  {
5138  j.LimitEnabled = joint.LimitEnabled;
5139  TryUpdateJointParam(j, "limitenabled", j.LimitEnabled);
5140  }
5141  j.UpperLimit = joint.UpperLimit;
5142  TryUpdateJointParam(j, "upperlimit", angle);
5143  }
5144  }
5145  if (allowPairEditing && limbPairEditing)
5146  {
5147  UpdateOtherJoints(limb, (otherLimb, otherJoint) =>
5148  {
5149  if (IsMatchingLimb(limb, otherLimb, joint, otherJoint))
5150  {
5151  if (otherJoint.LimitEnabled != joint.LimitEnabled)
5152  {
5153  otherJoint.LimitEnabled = otherJoint.LimitEnabled;
5154  TryUpdateJointParam(otherJoint, "limitenabled", otherJoint.LimitEnabled);
5155  }
5156  otherJoint.UpperLimit = joint.UpperLimit;
5157  TryUpdateJointParam(otherJoint, "upperlimit", angle);
5158  }
5159  });
5160  }
5161  DrawAngle(20, angleColor, 4);
5162  DrawAngle(40, Color.Cyan);
5163  GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUIStyle.SmallFont);
5164  }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition);
5165  DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle =>
5166  {
5167  joint.LowerLimit = MathHelper.ToRadians(angle);
5168  ValidateJoint(joint);
5169  angle = MathHelper.ToDegrees(joint.LowerLimit);
5170  TryUpdateJointParam(joint, "lowerlimit", angle);
5171  if (copyJointSettings)
5172  {
5173  foreach (var j in selectedJoints)
5174  {
5175  if (j.LimitEnabled != joint.LimitEnabled)
5176  {
5177  j.LimitEnabled = joint.LimitEnabled;
5178  TryUpdateJointParam(j, "limitenabled", j.LimitEnabled);
5179  }
5180  j.LowerLimit = joint.LowerLimit;
5181  TryUpdateJointParam(j, "lowerlimit", angle);
5182  }
5183  }
5184  if (allowPairEditing && limbPairEditing)
5185  {
5186  UpdateOtherJoints(limb, (otherLimb, otherJoint) =>
5187  {
5188  if (IsMatchingLimb(limb, otherLimb, joint, otherJoint))
5189  {
5190  if (otherJoint.LimitEnabled != joint.LimitEnabled)
5191  {
5192  otherJoint.LimitEnabled = otherJoint.LimitEnabled;
5193  TryUpdateJointParam(otherJoint, "limitenabled", otherJoint.LimitEnabled);
5194  }
5195  otherJoint.LowerLimit = joint.LowerLimit;
5196  TryUpdateJointParam(otherJoint, "lowerlimit", angle);
5197  }
5198  });
5199  }
5200  DrawAngle(20, angleColor, 4);
5201  DrawAngle(25, Color.Yellow);
5202  GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUIStyle.SmallFont);
5203  }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition);
5204  void DrawAngle(float radius, Color color, float thickness = 5)
5205  {
5206  float angle = joint.UpperLimit - joint.LowerLimit;
5207  float offset = clockWise ? rotationOffset + joint.LowerLimit - MathHelper.PiOver2 : rotationOffset - joint.UpperLimit - MathHelper.PiOver2;
5208  ShapeExtensions.DrawSector(spriteBatch, drawPos, radius, angle, 40, color, offset: offset, thickness: thickness);
5209  }
5210  }
5211 
5212  private void Nudge(Keys key)
5213  {
5214  switch (key)
5215  {
5216  case Keys.Left:
5217  foreach (var limb in selectedLimbs)
5218  {
5219  // Can't edit human heads
5220  if (limb.type == LimbType.Head && character.IsHuman) { continue; }
5221  var newRect = limb.ActiveSprite.SourceRect;
5222  bool resize = PlayerInput.KeyDown(Keys.LeftControl);
5223  if (resize)
5224  {
5225  if (lockSpriteSize) { return; }
5226  newRect.Width--;
5227  }
5228  else
5229  {
5230  if (lockSpritePosition) { return; }
5231  newRect.X--;
5232  }
5233  UpdateSourceRect(limb, newRect, resize);
5234  }
5235  break;
5236  case Keys.Right:
5237  foreach (var limb in selectedLimbs)
5238  {
5239  // Can't edit human heads
5240  if (limb.type == LimbType.Head && character.IsHuman) { continue; }
5241  var newRect = limb.ActiveSprite.SourceRect;
5242  bool resize = PlayerInput.KeyDown(Keys.LeftControl);
5243  if (resize)
5244  {
5245  if (lockSpriteSize) { return; }
5246  newRect.Width++;
5247  }
5248  else
5249  {
5250  if (lockSpritePosition) { return; }
5251  newRect.X++;
5252  }
5253  UpdateSourceRect(limb, newRect, resize);
5254  }
5255  break;
5256  case Keys.Down:
5257  foreach (var limb in selectedLimbs)
5258  {
5259  // Can't edit human heads
5260  if (limb.type == LimbType.Head && character.IsHuman) { continue; }
5261  var newRect = limb.ActiveSprite.SourceRect;
5262  bool resize = PlayerInput.KeyDown(Keys.LeftControl);
5263  if (resize)
5264  {
5265  if (lockSpriteSize) { return; }
5266  newRect.Height++;
5267  }
5268  else
5269  {
5270  if (lockSpritePosition) { return; }
5271  newRect.Y++;
5272  }
5273  UpdateSourceRect(limb, newRect, resize);
5274  }
5275  break;
5276  case Keys.Up:
5277  foreach (var limb in selectedLimbs)
5278  {
5279  // Can't edit human heads
5280  if (limb.type == LimbType.Head && character.IsHuman) { continue; }
5281  var newRect = limb.ActiveSprite.SourceRect;
5282  bool resize = PlayerInput.KeyDown(Keys.LeftControl);
5283  if (resize)
5284  {
5285  if (lockSpriteSize) { return; }
5286  newRect.Height--;
5287  }
5288  else
5289  {
5290  if (lockSpritePosition) { return; }
5291  newRect.Y--;
5292  }
5293  UpdateSourceRect(limb, newRect, resize);
5294  }
5295  break;
5296  }
5297  RagdollParams.StoreSnapshot();
5298  }
5299 
5300  private void SetSpritesheetRestriction(bool value)
5301  {
5302  unrestrictSpritesheet = value;
5303  CalculateSpritesheetZoom();
5304  spriteSheetZoomBar.BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom));
5305  }
5306 #endregion
5307 
5308 #region Widgets as methods
5309  private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, LocalizedString toolTip, Color color, Action<float> onClick,
5310  float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false, int rounding = 1)
5311  {
5312  var angle = value;
5313  if (!MathUtils.IsValid(angle))
5314  {
5315  angle = 0;
5316  }
5317  float drawAngle = clockWise ? angle : -angle;
5318  var widgetDrawPos = drawPos + VectorExtensions.Forward(MathHelper.ToRadians(drawAngle) + rotationOffset - MathHelper.PiOver2, circleRadius);
5319  GUI.DrawLine(spriteBatch, drawPos, widgetDrawPos, color);
5320  DrawWidget(spriteBatch, widgetDrawPos, WidgetType.Rectangle, widgetSize, color, toolTip, () =>
5321  {
5322  GUI.DrawLine(spriteBatch, drawPos, widgetDrawPos, color, width: 3);
5323  ShapeExtensions.DrawCircle(spriteBatch, drawPos, circleRadius, 40, color, thickness: 1);
5324  Vector2 d = PlayerInput.MousePosition - drawPos;
5325  float newAngle = clockWise
5326  ? MathUtils.VectorToAngle(d) + MathHelper.PiOver2 - rotationOffset
5327  : -MathUtils.VectorToAngle(d) - MathHelper.PiOver2 + rotationOffset;
5328  angle = MathHelper.ToDegrees(wrapAnglePi ? MathUtils.WrapAnglePi(newAngle) : MathUtils.WrapAngleTwoPi(newAngle));
5329  angle = (float)Math.Round(angle / rounding) * rounding;
5330  if (angle >= 360 || angle <= -360) { angle = 0; }
5331  if (displayAngle)
5332  {
5333  GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUIStyle.SmallFont);
5334  }
5335  onClick(angle);
5336  }, autoFreeze, holdPosition, onHovered: () =>
5337  {
5338  if (!PlayerInput.PrimaryMouseButtonHeld())
5339  {
5340  GUIComponent.DrawToolTip(
5341  spriteBatch,
5342  $"{toolTip} ({angle.FormatZeroDecimal()})",
5343  new Vector2(drawPos.X + 50, drawPos.Y - widgetSize / 2 - 50));
5344  }
5345  });
5346  }
5347 
5348  private enum WidgetType { Rectangle, Circle }
5349  private void DrawWidget(SpriteBatch spriteBatch, Vector2 drawPos, WidgetType widgetType, int size, Color color, LocalizedString toolTip, Action onPressed, bool? autoFreeze = null, bool holdPosition = false, Action onHovered = null)
5350  {
5351  var drawRect = new Rectangle((int)drawPos.X - size / 2, (int)drawPos.Y - size / 2, size, size);
5352  var inputRect = drawRect;
5353  inputRect.Inflate(size * 0.75f, size * 0.75f);
5354  bool isMouseOn = inputRect.Contains(PlayerInput.MousePosition);
5355  bool isSelected = isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None();
5356  switch (widgetType)
5357  {
5358  case WidgetType.Rectangle:
5359  if (isSelected)
5360  {
5361  var rect = drawRect;
5362  rect.Inflate(size * 0.3f, size * 0.3f);
5363  GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3, isFilled: PlayerInput.PrimaryMouseButtonHeld());
5364  }
5365  else
5366  {
5367  GUI.DrawRectangle(spriteBatch, drawRect, color, thickness: 1, isFilled: false);
5368  }
5369  break;
5370  case WidgetType.Circle:
5371  if (isSelected)
5372  {
5373  ShapeExtensions.DrawCircle(spriteBatch, drawPos, size * 0.7f, 40, color, thickness: 3);
5374  }
5375  else
5376  {
5377  ShapeExtensions.DrawCircle(spriteBatch, drawPos, size * 0.5f, 40, color, thickness: 1);
5378  }
5379  break;
5380  default: throw new NotImplementedException(widgetType.ToString());
5381  }
5382  if (isSelected)
5383  {
5384  // Label/tooltip
5385  if (onHovered == null)
5386  {
5387  GUIComponent.DrawToolTip(spriteBatch, toolTip, new Vector2(drawRect.Right + 5, drawRect.Y - drawRect.Height / 2));
5388  }
5389  else
5390  {
5391  onHovered();
5392  }
5393  if (PlayerInput.PrimaryMouseButtonHeld())
5394  {
5395  if (autoFreeze ?? this.autoFreeze)
5396  {
5397  isFrozen = true;
5398  }
5399  if (holdPosition == true)
5400  {
5401  character.AnimController.Collider.PhysEnabled = false;
5402  }
5403  onPressed();
5404  }
5405  else
5406  {
5407  isFrozen = freezeToggle.Selected;
5408  character.AnimController.Collider.PhysEnabled = true;
5409  }
5410  // Might not be entirely reliable, since the method is used inside the draw loop.
5411  if (PlayerInput.PrimaryMouseButtonClicked())
5412  {
5413  SaveSnapshot();
5414  }
5415  }
5416  }
5417 #endregion
5418 
5419 #region Widgets as classes
5420  private Dictionary<string, Widget> animationWidgets = new Dictionary<string, Widget>();
5421  private Dictionary<string, Widget> jointSelectionWidgets = new Dictionary<string, Widget>();
5422  private Dictionary<string, Widget> limbEditWidgets = new Dictionary<string, Widget>();
5423 
5424  private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, WidgetShape shape = WidgetShape.Rectangle, Action<Widget> initMethod = null)
5425  {
5426  string id = $"{character.SpeciesName}_{character.AnimController.CurrentAnimationParams.AnimationType.ToString()}_{name}";
5427  if (!animationWidgets.TryGetValue(id, out Widget widget))
5428  {
5429  int selectedSize = (int)Math.Round(size * sizeMultiplier);
5430  widget = new Widget(id, size, shape)
5431  {
5432  TooltipOffset = new Vector2(selectedSize / 2 + 5, -10),
5433  Data = character.AnimController.CurrentAnimationParams
5434  };
5435  widget.MouseUp += () => CurrentAnimation.StoreSnapshot();
5436  widget.Color = innerColor;
5437  widget.SecondaryColor = outerColor;
5438  widget.PreUpdate += dTime =>
5439  {
5440  widget.Enabled = editAnimations;
5441  if (widget.Enabled)
5442  {
5443  AnimationParams data = widget.Data as AnimationParams;
5444  widget.Enabled = data.AnimationType == character.AnimController.CurrentAnimationParams.AnimationType;
5445  }
5446  };
5447  widget.PostUpdate += dTime =>
5448  {
5449  widget.InputAreaMargin = widget.IsControlled ? 1000 : 0;
5450  widget.Size = widget.IsSelected ? selectedSize : size;
5451  widget.IsFilled = widget.IsControlled;
5452  };
5453  widget.PreDraw += (sp, dTime) =>
5454  {
5455  if (!widget.IsControlled)
5456  {
5457  widget.Refresh();
5458  }
5459  };
5460  animationWidgets.Add(id, widget);
5461  initMethod?.Invoke(widget);
5462  }
5463  return widget;
5464  }
5465 
5466  private Widget GetJointSelectionWidget(string id, LimbJoint joint, string linkedId = null)
5467  {
5468  // Handle widget linking and create the widgets
5469  if (!jointSelectionWidgets.TryGetValue(id, out Widget jointWidget))
5470  {
5471  jointWidget = CreateJointSelectionWidget(id, joint);
5472  if (linkedId != null)
5473  {
5474  if (!jointSelectionWidgets.TryGetValue(linkedId, out Widget linkedWidget))
5475  {
5476  linkedWidget = CreateJointSelectionWidget(linkedId, joint);
5477  }
5478  jointWidget.LinkedWidget = linkedWidget;
5479  linkedWidget.LinkedWidget = jointWidget;
5480  }
5481  }
5482  return jointWidget;
5483 
5484  // Widget creation method
5485  Widget CreateJointSelectionWidget(string ID, LimbJoint j)
5486  {
5487  int normalSize = 10;
5488  int selectedSize = 20;
5489  var widget = new Widget(ID, normalSize, WidgetShape.Circle);
5490  widget.Refresh = () =>
5491  {
5492  widget.ShowTooltip = !selectedJoints.Contains(joint);
5493  widget.Color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red;
5494  };
5495  widget.Refresh();
5496  widget.PreUpdate += dTime => widget.Enabled = editJoints;
5497  widget.PostUpdate += dTime =>
5498  {
5499  widget.InputAreaMargin = widget.IsControlled ? 1000 : 0;
5500  widget.Size = widget.IsSelected ? selectedSize : normalSize;
5501  };
5502  widget.MouseDown += () =>
5503  {
5504  if (jointCreationMode != JointCreationMode.None) { return; }
5505  if (!selectedJoints.Contains(joint))
5506  {
5507  if (!Widget.EnableMultiSelect)
5508  {
5509  selectedJoints.Clear();
5510  }
5511  selectedJoints.Add(joint);
5512  }
5513  else if (Widget.EnableMultiSelect)
5514  {
5515  selectedJoints.Remove(joint);
5516  }
5517  foreach (var w in jointSelectionWidgets.Values)
5518  {
5519  w.Refresh();
5520  w.LinkedWidget?.Refresh();
5521  }
5522  ResetParamsEditor();
5523  };
5524  widget.MouseUp += () =>
5525  {
5526  if (jointCreationMode == JointCreationMode.None)
5527  {
5528  RagdollParams.StoreSnapshot();
5529  }
5530  };
5531  widget.Tooltip = joint.Params.Name;
5532  widget.TooltipOffset = new Vector2(-GUIStyle.Font.MeasureString(widget.Tooltip).X - 30, -10);
5533  jointSelectionWidgets.Add(ID, widget);
5534  return widget;
5535  }
5536  }
5537 
5538  private Widget GetLimbEditWidget(string ID, Limb limb, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action < Widget> initMethod = null)
5539  {
5540  if (!limbEditWidgets.TryGetValue(ID, out Widget widget))
5541  {
5542  widget = CreateLimbEditWidget();
5543  limbEditWidgets.Add(ID, widget);
5544  }
5545  return widget;
5546 
5547  Widget CreateLimbEditWidget()
5548  {
5549  int normalSize = size;
5550  int selectedSize = (int)Math.Round(size * 1.5f);
5551  var w = new Widget(ID, size, shape)
5552  {
5553  TooltipOffset = new Vector2(selectedSize / 2 + 5, -10),
5554  Data = limb,
5555  Color = Color.Yellow,
5556  SecondaryColor = Color.Gray,
5557  TextColor = Color.Yellow
5558  };
5559  w.PreUpdate += dTime => w.Enabled = editLimbs && selectedLimbs.Contains(limb);
5560  w.PostUpdate += dTime =>
5561  {
5562  w.InputAreaMargin = w.IsControlled ? 1000 : 0;
5563  w.Size = w.IsSelected ? selectedSize : normalSize;
5564  w.IsFilled = w.IsControlled;
5565  };
5566  w.MouseUp += () => RagdollParams.StoreSnapshot();
5567  initMethod?.Invoke(w);
5568  return w;
5569  }
5570  }
5571 #endregion
5572  }
5573 }
static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
static string GetDefaultFile(Identifier speciesName, AnimationType animType)
abstract void StoreSnapshot()
static string GetDefaultFileName(Identifier speciesName, AnimationType animType)
static string GetFolder(Identifier speciesName)
Vector2 WorldToScreen(Vector2 coords)
Definition: Camera.cs:416
float? Zoom
Definition: Camera.cs:78
void MoveCamera(float deltaTime, bool allowMove=true, bool allowZoom=true, bool allowInput=true, bool? followSub=null)
Definition: Camera.cs:255
Matrix Transform
Definition: Camera.cs:136
void UpdateTransform(bool interpolate=true, bool updateListener=true)
Definition: Camera.cs:199
Vector2 Position
Definition: Camera.cs:398
bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config=null, IEnumerable< AnimationParams > animations=null)
static LocalizedString GetCharacterEditorTranslation(string tag)
Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll=null)
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
override void AddToGUIUpdateList()
By default, submits the screen's main GUIFrame and, if requested upon construction,...
void CopyExisting(CharacterParams character, RagdollParams ragdoll, IEnumerable< AnimationParams > animations)
Definition: Wizard.cs:31
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
void GiveJobItems(bool isPvPMode, WayPoint spawnPoint=null)
Stores information about the Character that is needed between rounds in the menu etc....
Contains character data that should be editable in the character editor.
bool Serialize(XElement element=null, bool alsoChildren=true, bool recursive=true)
static IEnumerable< ContentXElement > ConfigElements
static readonly Identifier HumanSpeciesName
static ContentPath FromRaw(string? rawValue)
string???????????? Value
Definition: ContentPath.cs:27
virtual Vector2 WorldPosition
Definition: Entity.cs:49
override Color Color
Definition: GUIButton.cs:41
override bool Enabled
Definition: GUIButton.cs:27
override Color HoverColor
Definition: GUIButton.cs:51
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual Rectangle Rect
RectTransform RectTransform
void Select(int index)
Definition: GUIDropDown.cs:376
override bool Enabled
Definition: GUITickBox.cs:40
override bool Selected
Definition: GUITickBox.cs:18
static int GraphicsWidth
Definition: GameMain.cs:162
static ContentPackage VanillaContent
Definition: GameMain.cs:84
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
static int GraphicsHeight
Definition: GameMain.cs:168
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static World World
Definition: GameMain.cs:105
Action ResolutionChanged
NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer...
Definition: GameMain.cs:133
static bool DebugDraw
Definition: GameMain.cs:29
static GameMain Instance
Definition: GameMain.cs:144
static bool DevMode
Doesn't automatically enable los or bot AI or do anything like that. Probably not fully implemented.
Definition: GameMain.cs:33
static Sounds.SoundManager SoundManager
Definition: GameMain.cs:80
static readonly PrefabCollection< JobPrefab > Prefabs
void DrawDamageModifiers(SpriteBatch spriteBatch, Camera cam, Vector2 startPos, bool isScreenSpace)
static readonly List< MapEntity > MapEntityList
static File FromPath(string path, Type type)
Prefer FromPath<T> when possible, this just exists for cases where the type can only be decided at ru...
static ParamsEditor Instance
Definition: ParamsEditor.cs:11
void SetPrevTransform(Vector2 simPosition, float rotation)
static bool KeyDown(InputType inputType)
bool Serialize(XElement element=null, bool alsoChildren=true, bool recursive=true)
static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage=null)
static void ClearCache()
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
const string SoundCategoryWaterAmbience
Definition: SoundManager.cs:17
void SetCategoryGainMultiplier(string category, float gain, int index=0)
static void DrawFront(SpriteBatch spriteBatch, bool editing=false, Predicate< MapEntity > predicate=null)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static void DrawBack(SpriteBatch spriteBatch, bool editing=false, Predicate< MapEntity > predicate=null)
static WayPoint GetRandom(SpawnType spawnType=SpawnType.Human, JobPrefab assignedJob=null, Submarine sub=null, bool useSyncedRand=false, string spawnPointTag=null, bool ignoreSubmarine=false)
NumberType
Definition: Enums.cs:741
CursorState
Definition: GUI.cs:40
WidgetShape
Definition: Widget.cs:11