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.

195 lines
7.5 KiB

  1. using Microsoft.Xna.Framework;
  2. using Microsoft.Xna.Framework.Graphics;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. namespace SemiColinGames {
  7. public class Terrain {
  8. public static Terrain FromSymbol(char symbol) {
  9. if (mapping.ContainsKey(symbol)) {
  10. return mapping[symbol];
  11. } else {
  12. return null;
  13. }
  14. }
  15. private readonly static Dictionary<char, Terrain> mapping = new Dictionary<char, Terrain>();
  16. public static Terrain Empty = new Terrain('\0', false, Textures.Grassland, 0, 0);
  17. public static Terrain Grass = new Terrain('=', true, Textures.Grassland, 3, 0);
  18. public static Terrain GrassL = new Terrain('<', true, Textures.Grassland, 2, 0);
  19. public static Terrain GrassR = new Terrain('>', true, Textures.Grassland, 4, 0);
  20. public static Terrain Rock = new Terrain('.', true, Textures.Grassland, 3, 1);
  21. public static Terrain RockL = new Terrain('[', true, Textures.Grassland, 1, 2);
  22. public static Terrain RockR = new Terrain(']', true, Textures.Grassland, 5, 2);
  23. public static Terrain WaterL = new Terrain('~', false, Textures.Grassland, 9, 2);
  24. public static Terrain WaterR = new Terrain('`', false, Textures.Grassland, 10, 2);
  25. public static Terrain Block = new Terrain('X', true, Textures.Ruins, 2, 0);
  26. public static Terrain Spike = new Terrain('^', true, Textures.Grassland, 11, 8);
  27. public static Terrain Wood = new Terrain('_', true, Textures.Grassland, 10, 3);
  28. public static Terrain WoodL = new Terrain('(', true, Textures.Grassland, 9, 3);
  29. public static Terrain WoodR = new Terrain(')', true, Textures.Grassland, 12, 3);
  30. public static Terrain WoodVert = new Terrain('|', false, Textures.Grassland, 9, 5);
  31. public static Terrain WoodVertL = new Terrain('/', false, Textures.Grassland, 9, 4);
  32. public static Terrain WoodVertR = new Terrain('\\', false, Textures.Grassland, 12, 4);
  33. public static Terrain WoodBottom = new Terrain('v', false, Textures.Grassland, 10, 5);
  34. public static Terrain FenceL = new Terrain('d', false, Textures.Grassland, 5, 4);
  35. public static Terrain Fence = new Terrain('f', false, Textures.Grassland, 6, 4);
  36. public static Terrain FencePost = new Terrain('x', false, Textures.Grassland, 7, 4);
  37. public static Terrain FenceR = new Terrain('b', false, Textures.Grassland, 8, 4);
  38. public static Terrain VineTop = new Terrain('{', false, Textures.Ruins, 12, 5);
  39. public static Terrain VineMid = new Terrain(';', false, Textures.Ruins, 12, 6);
  40. public static Terrain VineBottom = new Terrain('}', false, Textures.Ruins, 12, 7);
  41. public static Terrain GrassTall = new Terrain('q', false, Textures.Grassland, 13, 0);
  42. public static Terrain GrassShort = new Terrain('w', false, Textures.Grassland, 14, 0);
  43. public static Terrain Shoots = new Terrain('e', false, Textures.Grassland, 15, 0);
  44. public static Terrain Bush = new Terrain('r', false, Textures.Grassland, 13, 2);
  45. public static Terrain Mushroom = new Terrain('t', false, Textures.Grassland, 17, 2);
  46. public bool IsObstacle { get; private set; }
  47. public bool IsHarmful { get; private set; } = false;
  48. public TextureRef Texture { get; private set; }
  49. public Rectangle TextureSource { get; private set; }
  50. private Terrain(char symbol, bool isObstacle, TextureRef texture, int x, int y) {
  51. if (mapping.ContainsKey(symbol)) {
  52. throw new ArgumentException("already have a terrain with symbol " + symbol);
  53. }
  54. mapping[symbol] = this;
  55. IsObstacle = isObstacle;
  56. // TODO: don't hard-code just the one spike.
  57. if (symbol == '^') {
  58. IsHarmful = true;
  59. }
  60. Texture = texture;
  61. int size = World.TileSize;
  62. TextureSource = new Rectangle(x * size, y * size, size, size);
  63. }
  64. }
  65. class Tile {
  66. public Tile(Terrain terrain, Rectangle position) {
  67. Terrain = terrain;
  68. Position = position;
  69. }
  70. public Rectangle Position { get; private set; }
  71. public Terrain Terrain { get; private set; }
  72. public void Draw(SpriteBatch spriteBatch) {
  73. spriteBatch.Draw(
  74. Terrain.Texture.Get, Position.Location.ToVector2(), Terrain.TextureSource, Color.White);
  75. }
  76. }
  77. class World {
  78. public const int TileSize = 16;
  79. readonly Tile[] tiles;
  80. readonly Tile[] decorations;
  81. Player player;
  82. readonly NPC[] npcs = new NPC[1];
  83. // Size of World in terms of tile grid.
  84. private readonly int tileWidth;
  85. private readonly int tileHeight;
  86. public Player Player {
  87. get { return player; }
  88. }
  89. // Size of World in pixels.
  90. public int Width {
  91. get { return tileWidth * TileSize; }
  92. }
  93. public int Height {
  94. get { return tileHeight * TileSize; }
  95. }
  96. public World(string levelSpecification) {
  97. player = new Player();
  98. npcs[0] = new NPC(new Point(16 * 38, 16 * 12));
  99. var tilesList = new List<Tile>();
  100. var decorationsList = new List<Tile>();
  101. string[] worldDesc = levelSpecification.Substring(1).Split('\n');
  102. tileWidth = worldDesc.AsQueryable().Max(a => a.Length);
  103. tileHeight = worldDesc.Length;
  104. Debug.WriteLine("world size: {0}x{1}", tileWidth, tileHeight);
  105. for (int i = 0; i < tileWidth; i++) {
  106. for (int j = 0; j < tileHeight; j++) {
  107. if (i < worldDesc[j].Length) {
  108. char key = worldDesc[j][i];
  109. Terrain terrain = Terrain.FromSymbol(key);
  110. if (terrain != null) {
  111. var position = new Rectangle(i * TileSize, j * TileSize, TileSize, TileSize);
  112. Tile tile = new Tile(terrain, position);
  113. if (tile.Terrain.IsObstacle) {
  114. tilesList.Add(tile);
  115. } else {
  116. decorationsList.Add(tile);
  117. }
  118. }
  119. }
  120. }
  121. }
  122. tiles = tilesList.ToArray();
  123. decorations = decorationsList.ToArray();
  124. // Because we added tiles from left to right, the CollisionTargets are sorted by x-position.
  125. // We maintain this invariant so that it's possible to efficiently find CollisionTargets that
  126. // are nearby a given x-position.
  127. CollisionTargets = new AABB[tiles.Length + 2];
  128. // Add a synthetic collisionTarget on the left side of the world.
  129. CollisionTargets[0] = new AABB(new Vector2(-1, 0), new Vector2(1, float.MaxValue));
  130. // Now add all the normal collisionTargets for every static terrain tile.
  131. Vector2 halfSize = new Vector2(TileSize / 2, TileSize / 2);
  132. for (int i = 0; i < tiles.Length; i++) {
  133. Vector2 center = new Vector2(
  134. tiles[i].Position.Left + halfSize.X, tiles[i].Position.Top + halfSize.Y);
  135. CollisionTargets[i + 1] = new AABB(center, halfSize, tiles[i].Terrain);
  136. }
  137. // Add a final synthetic collisionTarget on the right side of the world.
  138. CollisionTargets[tiles.Length + 1] = new AABB(
  139. new Vector2(Width + 1, 0), new Vector2(1, float.MaxValue));
  140. }
  141. public void Update(float modelTime, History<Input> input) {
  142. player.Update(modelTime, input, CollisionTargets);
  143. foreach (NPC npc in npcs) {
  144. npc.Update(modelTime);
  145. }
  146. if (player.Health <= 0) {
  147. Reset();
  148. }
  149. }
  150. void Reset() {
  151. player = new Player();
  152. }
  153. public void DrawBackground(SpriteBatch spriteBatch) {
  154. foreach (Tile t in decorations) {
  155. t.Draw(spriteBatch);
  156. }
  157. foreach (NPC npc in npcs) {
  158. npc.Draw(spriteBatch);
  159. }
  160. }
  161. public void DrawForeground(SpriteBatch spriteBatch) {
  162. foreach (Tile t in tiles) {
  163. t.Draw(spriteBatch);
  164. }
  165. }
  166. public AABB[] CollisionTargets { get; }
  167. }
  168. }