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