A stealth-based 2D platformer where you don't have to kill anyone unless you want to. https://www.semicolin.games
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

129 lines
4.4 KiB

  1. using Microsoft.Xna.Framework;
  2. using Microsoft.Xna.Framework.Content;
  3. using System.Collections.Generic;
  4. using System.Text.Json;
  5. namespace SemiColinGames {
  6. public static class Sprites {
  7. public static Sprite Executioner;
  8. public static Sprite Ninja;
  9. public static void Load(ContentManager content) {
  10. Executioner = new Sprite(
  11. Textures.Executioner, content.LoadString("sprites/ccg/executioner_female.json"));
  12. Ninja = new Sprite(
  13. Textures.Ninja, content.LoadString("sprites/ccg/ninja_female.json"));
  14. }
  15. }
  16. enum AnimationDirection {
  17. Forward,
  18. PingPong
  19. }
  20. struct SpriteAnimation {
  21. public readonly int Start;
  22. public readonly int End;
  23. public readonly double Duration;
  24. public readonly AnimationDirection Direction;
  25. public SpriteAnimation(int start, int end, double duration, AnimationDirection direction) {
  26. Start = start;
  27. End = end;
  28. Duration = duration;
  29. Direction = direction;
  30. }
  31. }
  32. struct Frame {
  33. public readonly Rectangle Source;
  34. public readonly double Duration;
  35. public Frame(Rectangle source, double duration) {
  36. Source = source;
  37. Duration = duration;
  38. }
  39. }
  40. public class Sprite {
  41. public readonly int Width;
  42. public readonly int Height;
  43. // Measures the empty pixels between the ground (where the sprite's feet rest in idle pose)
  44. // and the bottom of the sprite's image.
  45. public readonly int GroundPadding = 7;
  46. public readonly TextureRef Texture;
  47. private readonly Dictionary<string, SpriteAnimation> animations;
  48. private readonly List<Frame> frames;
  49. public Sprite(TextureRef texture, string metadataJson) {
  50. Texture = texture;
  51. animations = new Dictionary<string, SpriteAnimation>();
  52. JsonElement jsonRoot = JsonDocument.Parse(metadataJson).RootElement;
  53. frames = new List<Frame>();
  54. foreach (JsonElement child in jsonRoot.GetProperty("frames").EnumerateArray()) {
  55. JsonElement frame = child.GetProperty("frame");
  56. Rectangle source = new Rectangle(
  57. frame.GetProperty("x").GetInt32(),
  58. frame.GetProperty("y").GetInt32(),
  59. frame.GetProperty("w").GetInt32(),
  60. frame.GetProperty("h").GetInt32());
  61. double duration = child.GetProperty("duration").GetDouble() / 1000;
  62. frames.Add(new Frame(source, duration));
  63. }
  64. // We assume that all frames are the same size (which right now is assured by the
  65. // Aseprite-based spritesheet export process).
  66. Width = frames[0].Source.Width;
  67. Height = frames[0].Source.Height;
  68. JsonElement frameTags = jsonRoot.GetProperty("meta").GetProperty("frameTags");
  69. foreach (JsonElement child in frameTags.EnumerateArray()) {
  70. string name = child.GetProperty("name").GetString();
  71. int start = child.GetProperty("from").GetInt32();
  72. int end = child.GetProperty("to").GetInt32();
  73. string directionString = child.GetProperty("direction").GetString();
  74. AnimationDirection direction = directionString == "pingpong" ?
  75. AnimationDirection.PingPong : AnimationDirection.Forward;
  76. double duration = 0;
  77. for (int i = start; i <= end; i++) {
  78. duration += frames[i].Duration;
  79. }
  80. // A PingPong animation repeats every frame but the first and last.
  81. // Therefore its duration is 2x, minus the duration of the first and last frames.
  82. if (direction == AnimationDirection.PingPong) {
  83. duration = duration * 2 - frames[start].Duration - frames[end].Duration;
  84. }
  85. animations[name] = new SpriteAnimation(start, end, duration, direction);
  86. }
  87. }
  88. public Rectangle GetTextureSource(string animationName, double time) {
  89. SpriteAnimation animation = animations[animationName];
  90. time %= animation.Duration;
  91. for (int i = animation.Start; i <= animation.End; i++) {
  92. double frameTime = frames[i].Duration;
  93. if (time < frameTime) {
  94. return frames[i].Source;
  95. }
  96. time -= frameTime;
  97. }
  98. if (animation.Direction == AnimationDirection.PingPong) {
  99. for (int i = animation.End - 1; i > animation.Start; i--) {
  100. double frameTime = frames[i].Duration;
  101. if (time < frameTime) {
  102. return frames[i].Source;
  103. }
  104. time -= frameTime;
  105. }
  106. }
  107. // We shouldn't get here, but if we did, the last frame is a fine thing to return.
  108. return frames[animation.End].Source;
  109. }
  110. }
  111. }