Spread the love

Unfortunately, I have suffered quite a ton of difficulties that disrupted my functionality. Hence, apologies that we missed our first-week last week, and that I haven’t been able to complete February’s book. The book will be done eventually, and I figured I should make sure that despite not finishing everything I needed to complete we should press on and share the DotE progress.

Dungeon of the Endless is a Roguelike I am slowly working on. Take note, slowly… working on… I don’t rush through making things, I don’t just go “ooh, let’s add that feature,” to keep it manageable it will have delayed progress reports.

Take note, this was somewhat usable when I wanted to share it on Make Games SA. That didn’t cut it for me, I needed quite a lot of changes. Unfortunately, it is already there so you can take a look at it, it just needs a lot of changes… I have finally started that for myself.

Initialize New Repo

So, I had it as a single project built using MonoGame for Windows 10 initially for the post. I got myself a private git repo (only on my PC still, despite Github allowing free private repositorys now) and did the dance to start the wrapping I want for the engine code.

    public class DotEGame : Game
    {
        private static DotEGame _instance;
        public static DotEGame Instance { get { return _instance; } }

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        EngineContent _engineContent;

        List _states;
        public List States
        {
            get { return _states; }
        }
        
        public DotEGame()
        {
            _instance = this;
            IsMouseVisible = true;

            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            _engineContent = new EngineContent(Content);

            _states = new List();
        }
        
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            _states.Add(new StateMenu());
            _states[0].Load();
        }
        
        protected override void Update(GameTime gameTime)
        {
            // Keyboard and Mouse states are managed within each state we open
            KeyboardState keyboard = Keyboard.GetState();
            MouseState mouse = Mouse.GetState();

            if (_states[_states.Count - 1].Loaded)
            {
                _states[_states.Count - 1].Update(gameTime, keyboard, mouse);
            }

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            _states[_states.Count - 1].Draw(gameTime, spriteBatch);

            base.Draw(gameTime);
        }

        ///
        /// Personally Unused
        /// 
        protected override void UnloadContent() { }
        protected override void Initialize() { base.Initialize(); }
    }

We initialize states and a way to use them, this is so that separate code may be used for a menu or some gameplay. I have ideas that would like to have this with a single starting state for gameplay. This isn’t completely correct as the game’s mechanics need a lot of work, however, we first pay attention to the states.

   /// 
    /// This is an abstract idea of "interface", not the C# wrapping
    /// 
    public abstract class IGameState
    {
        // Reference for "loading screen" structure
        private bool _loaded = false;
        public bool Loaded { get { return _loaded; } set { _loaded = value; } }

        private Texture2D _loadingScreen;
        public Texture2D LoadingScreen { get { return _loadingScreen; } }

        public IGameState(string texLoad)
        {
            _loadingScreen = EngineContent.Instance.Load(texLoad);
        }

        /// 
        /// Load Content
        ///  - set "_loaded" true when done
        ///  - note: use threads to be different
        /// 
        public abstract void Load();

        /// 
        /// Logic like the update time within the game state
        /// 
        /// input gameTime
        /// input keyboard.GetState()
        /// input mouse.GetState()
        public abstract void Update(GameTime gameTime, KeyboardState keyboard, MouseState mouse);

        /// 
        /// Logic like the draw time within the game state
        /// 
        /// input gameTime
        /// input spriteBatch
        public abstract void Draw(GameTime gameTime, SpriteBatch spriteBatch);

Keeping it simple, we can have separate code for each state of the game’s running. You will notice we have some additional helpers.

You will see there how we manage that Load.

        // Reference for "loading screen" structure
        private bool _loaded = false;
        public bool Loaded { get { return _loaded; } set { _loaded = value; } }

        public IGameState(string texLoad)
        {
            _loadingScreen = EngineContent.Instance.Load(texLoad);
        }

As you see, we use a simple structure for letting the Game know if something is finished loading. In the Update, we will only Update the state when the loading is finished. You will see more of that when we begin looking at the actual states, this is just to point out there is slightly different reasoning for loading states.

Next, we can make that EngineContent code for ourselves, so you can see how it all works out together.

    /// 
    /// Note: we use the XNA similar coding in here to build this
    /// 
    public class EngineContent
    {
        private static EngineContent _instance;
        public static EngineContent Instance { get { return _instance; } }

        internal void MoveDraw(int x, int y, int w, int h)
        {
            _drawRect.X = x;
            _drawRect.Y = y;
            _drawRect.Width = w;
            _drawRect.Height = h;
        }

        private ContentManager _content;
        public EngineContent(ContentManager content)
        {
            _instance = this;
            _content = content;
        }

        private Dictionary _texture2DDictionary = new Dictionary();
        public Texture2D Load(string name)
        {
            if (_texture2DDictionary.ContainsKey(name)) return _texture2DDictionary[name];

            _texture2DDictionary.Add(name, _content.Load(name));
            return _texture2DDictionary[name];
        }

        private Rectangle _drawRect = new Rectangle(0,0,800,600);
        public Rectangle DrawRect
        {
            get { return _drawRect; }
            set { _drawRect = value; }
        }
    }

Note that we have DrawRect here, I would love thoughts on using the single rectangle. Surely we should rather use multiple rectangles instead of just moving one over, and over. Essentially this doesn’t have all the resources we want to be adding, for example, we will need fonts in the future.

The main idea for the loading to be like this is quite simple, I would like to have an easy method for freeing memory at a later stage. This way we can keep certain content loaded, and eventually, we can have background “cleanup” for ourselves. Cleanup taking things out of memory, and freeing space as needed. Currently we don’t need that, I just figure it would be better to have that possible from the beginning.

The logic behind this is quite simple, we want to have a single way of managing all content loading (and eventually unloading). We would love to potentially have completely different states for how the game runs. Ignoring that, this way our code is more manageable for ourselves. When we are looking for “the bug in the ‘credits’ game state,” we only need to look through a tiny event we made in the “credits” game state.

Similarly, for each tile we have an enum for “tiles” for ourselves. This doesn’t bring much, it is just for the drawing code we need in the “play state”.

You will have seen in the image above, this is in Visual Studio 2017, I manually through command line created the repository in a folder. The screenshot is from after the next step of code, so don’t worry about not seeing some of the other code.

Starting Code: Game’s Classes

We can initially just pay attention to the code that was working before for the wrapping. The initial “gameplay” happens to be you moving around on a map, there is a “hound” that generates in spots on the map as well. We don’t start directly at the initial gameplay.

Now, you should obviously already understand the content is loaded through the MonoGame content manager. We first make the state for the menu:

    public class StateMenu : IGameState
    {
        private Texture2D _playButton;

        public StateMenu() : base("loadMenu")
        {

        }

        public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Begin();
            if (Loaded)
            {
                EngineContent.Instance.MoveDraw(32, 32, _playButton.Width, _playButton.Height);
                spriteBatch.Draw(_playButton, EngineContent.Instance.DrawRect, Color.White);
            }
            else
            {
                EngineContent.Instance.MoveDraw(0, 0, 800, 600);
                spriteBatch.Draw(LoadingScreen, EngineContent.Instance.DrawRect, Color.White);
            }
            spriteBatch.End();
        }

        public override void Load()
        {
            var t = new Thread(() =>
            {
                _playButton = EngineContent.Instance.Load("t_button_play");

                Loaded = true;
            });
            t.Start();
        }

        public override void Update(GameTime gameTime, KeyboardState keyboard, MouseState mouse)
        {
            if (Loaded)
            {
                EngineContent.Instance.MoveDraw(32, 32, _playButton.Width, _playButton.Height);
                if (EngineContent.Instance.DrawRect.Contains(mouse.X, mouse.Y) && mouse.LeftButton == ButtonState.Pressed)
                {
                    DotEGame.Instance.States.Add(new StatePlay());
                }
            }
        }
    }

This here can help you understand what was mentioned in the creation of the engine code. Within Load is the important part. We load content in a separate thread, and you will see in Update and Draw we pay attention to if it is loaded or not for our logic. You will remember in the IGameState you saw before, when we create the class we immediately load the load screen image for ourselves.

We could do effects, and more, here. I just figure I will make it initially simple, we don’t need to impress people with the load screens. We just need to allow our users to see that we are actually loading things. Hence, the play state happens to be empty for this step. We don’t need the game playable yet.

In a weird way, I have decided that since I am looking over Dungeons and Dragons 5E rules regularly, I want to get ideas from their systems. That is why it was quite simple, we need a D20.

    public static class D20
    {
        public enum DiceResult : int
        {
            CritFail = 1,
            D2,
            D3,
            D4,
            D5,
            D6,
            D7,
            D8,
            D9,
            D10,
            D11,
            D12,
            D13,
            D14,
            D15,
            D16,
            D17,
            D18,
            D19,
            CritSuccess
        };

        private static Random _rand = new Random(); // Dice rolls are guaranteed random

        public static DiceResult Roll()
        {
            var roll = _rand.Next(20) + 1;
            return (DiceResult)roll;
        }
    }

This choice came for the simplicity. I want to make the system need manageable rules. Certain roguelikes have stats that climb to infinity, imagine you start it with 12 HP to your name. You get to the 10th final level of the dungeon, you have 2,000 health. I decided we will stick to simpler numbers for ourselves. Level 1: 8 Hp… Level 10: 80 Hp.

This way we can manage the difficulty in simple ways, you might be weak to lightning damage, therefore an attack roll would roll twice and choose higher number against you. You have resistance to fire attacks as you are a fire mage, the roll against you would roll twice and keep the lower value. It is simple for mechanics and balance, I can slowly but surely get that working for us.

From that you can obviously understand the Engine’s Entity:

    public enum CharSprite
    {
        Player,
        Hound
    }

    public enum CharStat
    {
        Strength,
        Dexterity,
        Intelligence,
        Wisdom,
        Charisma
    }

    public abstract class Entity
    {
        public Entity(int sStrength, int sDexterity, int sIntelligence, int sWisdom, int sCharisma)
        {
            _stat_strength = sStrength;
            _stat_dexterity = sDexterity;
            _stat_intelligence = sIntelligence;
            _stat_wisdom = sWisdom;
            _stat_charisma = sCharisma;
        }

        private int _x = 0;
        public int X { get { return _x; } set { _x = value; } }

        private int _y = 0;
        public int Y { get { return _y; } set { _y = value; } }

        private bool _alive = true;
        public bool Alive { get { return _alive; } }

        private int _hp = 10;
        public int HP { get { return _hp; } }

        private int _max_hp = 10;
        public int MaxHP { get { return _max_hp; } }

        public abstract CharSprite VisibleAs();
        public abstract void UpdateMe();
        public abstract void TakeTurn();

        private int _stat_strength = 1;
        public int StatStrength { get { return _stat_strength; } set { _stat_strength = value; } }

        private int _stat_dexterity = 1;
        public int StatDexterity { get { return _stat_dexterity; } set { _stat_dexterity = value; } }
        public int StatDexMod { get { return (StatDexterity / 2) - 10; } }

        public int RollInitiative()
        {
            var got = D20.Roll();
            switch (got)
            {
                // Thought: is it dangerous for 99? Should I hit 9999?
                case D20.DiceResult.CritSuccess: return 99;
                case D20.DiceResult.CritFail: return -99;
            }

            return StatDexMod + (int)got;
        }

        private int _stat_intelligence = 1;
        public int StatIntelligence { get { return _stat_intelligence; } set { _stat_intelligence = value; } }

        private int _stat_wisdom = 1;
        public int StatWisdom { get { return _stat_wisdom; } set { _stat_wisdom = value; } }

        private int _stat_charisma = 1;
        public int StatCharisma { get { return _stat_charisma; } set { _stat_charisma = value; } }

        private bool _stat_surprised = false;
        public bool StatSurprised { get { return _stat_surprised; } set { _stat_surprised = value; } }

        // Rethink Reaction one day, perhaps it would be a feature?
        // Rethink what it means for you to move out of range, attack op?

        private void SetPos(int x, int y)
        {
            _x = x;
            _y = y;
        }
    }

Keeping it minimal, simple, and hopefully understandable. This way we can have separate entities coding, if Hounds get too powerful we can debug them slightly. It is slightly incorrect for player’s starting choices, however, that should be fine for the initial versions.

    public class PlayerEntity : Entity
    {
        public PlayerEntity(int sStrength, int sDexterity, int sIntelligence, int sWisdom, int sCharisma) : base(sStrength, sDexterity, sIntelligence, sWisdom, sCharisma)
        {

        }

        public override void TakeTurn()
        {
            throw new Exception("TakeTurn is automatic for non-player entities, it should never run for the player by accident.");
        }

        public override void UpdateMe()
        {
            throw new NotImplementedException();
        }

        public override CharSprite VisibleAs()
        {
            return CharSprite.Player;
        }

        internal void SetCoords(Point player_pos)
        {
            X = player_pos.X;
            Y = player_pos.Y;
        }
    }

Initializing player code can help you understand how this is used. You might ask “why can’t we take a turn as a player?” It is quite simple since I figure that should be more in the play state code, it should manage the turns for us. For the other entities we can call their behaviors:

    public interface IBehaviour
    {
        void UpdateMe(Entity me);
    }

    public static class Behaviours
    {
        private static BehaviourHound _behHound;
        public static BehaviourHound Hound
        {
            get
            {
                if (_behHound == null) _behHound = new BehaviourHound();
                return _behHound;
            }
        }
    }

There are tons of better ways we can do this, we just want it to be simple today. Similarly, I am possibly missing sharing certain things here.

Starting Play

This is what you will have seen from the post on MGSA, this doesn’t have any changes besides being in a git repository with a better structure for the project. We will be doing tons of work after this step was complete for the update.

    public class StatePlay : IGameState
    {
        private int _seed;
        private Random _rand;
        public Random Rand { get { return _rand; } }
        private int[,] _game_board;
        string debugstrng;

        public enum TurnState
        {
            Move1,
            Move2,
            Move3,
            Attack,
            Enemy
        }

        private TurnState _turnState = TurnState.Move1;
        public TurnState Turn { get { return _turnState; } }

To start out, as you will see that we manage the play in a very simple form. You can start to see some of what we give the player. They can move 0,1,2, or max 3 blocks, then they must attack. If they can’t attack it moves to the Enemy step, if they can it waits for them to attack. Enemy attacks here are simple, it is their complete update step using their behaviors that we showed a bit of before.

<pre><code>        public StatePlay(int seed) : base("loadMenu")
        {
            // When we generate the next seed we use something to do with this seed
            _seed = seed;

            debugstrng = "";

            // create map
            _game_board = new int[128, 128];
            _rand = new Random(0/*seed*/); //0 is starter seed so we can finally test the complete setup of initial game
            int prev_x = -1, prev_y = -1;
            int tiles = 3 + _rand.Next(10); // 3 -> 12
            List<Point> _map_gen_points = new List<Point>();
            List<Point> _map_gen_spawn_points = new List<Point>();
            for (int i = 0; i < tiles; i++)
            {
                // create a tile
                int tile_type = _rand.Next(1);
                /* TODO: different types of tiles */
                switch (tile_type)
                {
                    default: // 0 aswell
                        int r = 5 + _rand.Next(10);
                        int x = 0;
                        int y = 0;

                        bool is_spot_safe = false;
                        do
                        {
                            x = _rand.Next(r, 128 - r);
                            y = _rand.Next(r, 128 - r);
                            is_spot_safe = true;
                            for (int p = 0; p < _map_gen_points.Count; p++)
                            {
                                var p_val = _map_gen_points[p];
                                if (Math.Sqrt((x - p_val.X) * (x - p_val.X) + (y - p_val.Y) * (y - p_val.Y)) < 25)
                                    is_spot_safe = false;
                            }
                        } while (is_spot_safe == false);

                        _map_gen_points.Add(new Point(x, y));


                        for (int inner_x = x - r; inner_x < x + r; inner_x++)
                        {
                            for (int inner_y = y - r; inner_y < y + r; inner_y++)
                            {
                                _game_board[inner_x, inner_y] = (int)EnumTiles.Grass;
                                if (_rand.Next(100) == 0) _map_gen_spawn_points.Add(new Point(inner_x, inner_y));
                            }
                        }

                        //debugstrng += x.ToString() + ", " + y.ToString() + " (R: " + r.ToString() + ")\n";
                        if (x < 0 || x >= 128 || y < 0 || y >= 128) debugstrng += "^ error above\n";
                        if (prev_x != -1)
                        {
                            // draw a line between them
                            double a_y_over_x = ((y * 1.0f - prev_y) / (x * 1.0f - prev_x)); // y/x
                            double a = Math.Atan2((y * 1.0f - prev_y), (x * 1.0f - prev_x)); // the angle between x,y and prev_x,prev_y
                            double length = Math.Sqrt((x - prev_x) * (x - prev_x) + (y - prev_y) * (y - prev_y)); // Find length between center of prev and this
                                                                                                                  //debugstrng += "y/x=" + a_y_over_x.ToString() + "        a=" + a.ToString() + "        len=" + length.ToString() + "\n";
                            for (int l = 1; l < length; l++) // Go from ~0,0 and work out where the points go
                            {
                                int line_x = (int)(l * Math.Cos(a));
                                int line_y = (int)(l * Math.Sin(a));

                                for (int line_inner_x = -1; line_inner_x <= 1; line_inner_x++)
                                {
                                    for (int line_inner_y = -1; line_inner_y <= 1; line_inner_y++)
                                    {
                                        if (prev_x + line_x + line_inner_x < 0 ||
                                            prev_x + line_x + line_inner_x >= 128 ||
                                            prev_y + line_y + line_inner_y < 0 ||
                                            prev_y + line_y + line_inner_y >= 128)
                                            debugstrng += " > error in build line " + (prev_x + line_x + line_inner_x).ToString() + ", " + (prev_y + line_y + line_inner_y).ToString() + "\n";
                                        else
                                        {
                                            if (_rand.Next(100) == 0) _map_gen_spawn_points.Add(new Point(prev_x + line_x + line_inner_x, prev_y + line_y + line_inner_y));
                                            _game_board[(int)(prev_x + line_x + line_inner_x), (int)(prev_y + line_y + line_inner_y)] = (int)EnumTiles.Grass;
                                        }
                                    }
                                }
                            }
                        }

                        prev_x = x;
                        prev_y = y;
                        break;
                }
            }

            // Generate the map
            // TODO: color tiles (e.g. grass, dirt, water etc)
            var size = 128 * 128;
            Color[] mapcolors = new Color[size];
            for (var i = 0; i < size; i++) mapcolors[i] = Color.Transparent;
            for (var map_x = 0; map_x < 128; map_x++)
            {
                for (var map_y = 0; map_y < 128; map_y++)
                {
                    if (_game_board[map_x, map_y] != 0) // not a blank spot
                    {
                        mapcolors[map_x + 128 * map_y] = Color.LightGreen; // incomplete
                    }
                }
            }
            var tex = new Texture2D(DotEGame.Instance.GraphicsDevice, 128, 128, false, SurfaceFormat.Color);
            tex.SetData(mapcolors);
            _map = tex;

            // sprites
            _rec_tile_pos = new Rectangle(0, 0, 36, 36);
            _rec_map_pos = new Rectangle(663, 9, 128, 128);
            _rec_map_bg_pos = new Rectangle(660, 6, 134, 134);
            _rec_map_view_pos = new Rectangle(660, 6, 24, 18);
            _rec_entity_pos = new Rectangle(0, 0, 32, 32);
            EngineContent.Instance.Load<Texture2D>("t_grass");
            //EngineContent.Instance.Load<Texture2D>("gamefont");
            EngineContent.Instance.Load<Texture2D>("t_map_bg");
            EngineContent.Instance.Load<Texture2D>("t_map_view_pos");

            // Entity sprites
            EngineContent.Instance.Load<Texture2D>("t_char_sprite");

            if (File.Exists("deb.txt")) File.Delete("deb.txt");
            var b = File.CreateText("deb.txt");
            b.Write(debugstrng);
            b.Close();

            // Others
            int start_tile = _rand.Next(_map_gen_points.Count);
            visionPosition = new Vector2(-_map_gen_points[start_tile].X * 32 + 32 * 13, -_map_gen_points[start_tile].Y * 32 + 32 * 6);

            // Place player
            var player_pos = _map_gen_points[start_tile];
            _player = new PlayerEntity(10, 10, 10, 10, 10); // TODO: choose class or random class of player
            _player.SetCoords(player_pos);

            // TODO: generate enemies
            _game_entities = new List<Entity>();
            for (int i = 0; i < 25; i++)
            {
                // TODO: make character start position
                var pos_i = _rand.Next(_map_gen_spawn_points.Count);
                var pos_val = _map_gen_spawn_points[pos_i];
                _map_gen_spawn_points.RemoveAt(pos_i);
                _game_entities.Add(new HoundEntity(pos_val.X, pos_val.Y));
            }

            _drawable_entitys = new List<Entity>();
        }</code></pre>

The first stumbling block in showing this off, this is a direct copy of I used before when starting the project. That is why we don’t do anything for the load, we will move this away and into the load for ourselves “off camera”.

We then pick spots on the map, place rectangles, and the paths to the previous rectangle. This is a super simple generation, not too much. This is very simple, we needed a map for starting to implement things here. Then the position generation is here for the player and spawns.

       public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Begin();

            for (int inner_x = 0; inner_x < 128; inner_x++)
            {
                for (int inner_y = 0; inner_y < 128; inner_y++)
                {
                    _rec_tile_pos.X = (inner_x * 32 - 4) + (int)visionPosition.X;
                    _rec_tile_pos.Y = (inner_y * 32 - 4) + (int)visionPosition.Y;
                    if ((_rec_tile_pos.X > -32) && (_rec_tile_pos.X < 832) &&
                        (_rec_tile_pos.Y > -32) && (_rec_tile_pos.Y < 632)) // If tile is in 800x600 view rec we render
                    {
                        switch (_game_board[inner_x, inner_y])
                        {
                            case 1: // grass
                                EngineContent.Instance.DrawSprite(spriteBatch, _rec_tile_pos, "t_grass");
                                break;
                        }
                    }
                }
            }

            // TODO: draw all entities here
            while (_drawable_entitys.Count > 0) _drawable_entitys.RemoveAt(0);
            for (int j = 0; j < _game_entities.Count; j++)
            {
                _rec_tile_pos.X = (_game_entities[j].X * 32 - 2) + (int)visionPosition.X;
                _rec_tile_pos.Y = (_game_entities[j].Y * 32 - 2) + (int)visionPosition.Y;
                if ((_rec_tile_pos.X > -32) && (_rec_tile_pos.X < 832) &&
                    (_rec_tile_pos.Y > -32) && (_rec_tile_pos.Y < 632))
                {
                    _drawable_entitys.Add(_game_entities[j]);
                }
            }
            for (int k = 0; k < _drawable_entitys.Count; k++)
            {
                _rec_entity_pos.X = (_drawable_entitys[k].X * 32 - 2) + (int)visionPosition.X;
                _rec_entity_pos.Y = (_drawable_entitys[k].Y * 32 - 2) + (int)visionPosition.Y;
                var charSprite = _drawable_entitys[k].VisibleAs();
                switch (charSprite)
                {
                    case CharSprite.Hound: // note: my need default for some reason eventually
                        EngineContent.Instance.DrawSprite(spriteBatch, _rec_entity_pos, "t_hound");
                        break;
                }
            }

            // draw player
            _rec_entity_pos.X = (_player.X * 32 - 2) + (int)visionPosition.X;
            _rec_entity_pos.Y = (_player.Y * 32 - 2) + (int)visionPosition.Y;
            if ((_rec_entity_pos.X > -32) && (_rec_entity_pos.X < 832) &&
                (_rec_entity_pos.Y > -32) && (_rec_entity_pos.Y < 632))
            {
                EngineContent.Instance.DrawSprite(spriteBatch, _rec_entity_pos, "t_char_sprite");
            }

            EngineContent.Instance.DrawSprite(spriteBatch, _rec_map_bg_pos, "t_map_bg");
            spriteBatch.Draw(_map, _rec_map_pos, Color.White);
            EngineContent.Instance.DrawSprite(spriteBatch, _rec_map_view_pos, "t_map_view_pos");
            //SpriteManager.SpriteFontDraw(new Vector2(0, 0), "gamefont", visionPosition.X.ToString() + ", " + visionPosition.Y.ToString());
            //EngineContent.Instance.DrawString(spriteBatch, new Vector2(0, 0), "gamefont", debugstrng);

            spriteBatch.End();
        }

As you can see, we work out what is in vision on the map (though this may need slight adjustments). We also manage the drawn map on the top right. This was pretty much all done in the previous version once again.

        Texture2D _map;
        Rectangle _rec_map_pos;
        Rectangle _rec_map_bg_pos;
        Rectangle _rec_tile_pos;
        Rectangle _rec_map_view_pos;
        Rectangle _rec_entity_pos;
        List _drawable_entitys;

        float visionSpeed = 480f;
        Vector2 visionPosition;
        private PlayerEntity _player;
        private List _game_entities;
        private KeyboardState _prev_keyboard;

        public override void Update(GameTime gameTime, KeyboardState keyboard, MouseState mouse)
        {
            _rec_map_view_pos.X = -(int)(visionPosition.X / 32) + 663;
            _rec_map_view_pos.Y = -(int)(visionPosition.Y / 32) + 9;

            if ((keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.A)) && (keyboard.IsKeyDown(Keys.S) || keyboard.IsKeyDown(Keys.W)))
            {
                visionSpeed = 320f;
            }
            else
            {
                visionSpeed = 480f;
            }

            if (keyboard.IsKeyDown(Keys.D))
            {
                if (visionPosition.X > -4128)
                    visionPosition.X -= visionSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }
            else if (keyboard.IsKeyDown(Keys.A))
            {
                if (visionPosition.X < 32)
                    visionPosition.X += visionSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }

            if (keyboard.IsKeyDown(Keys.S))
            {
                if (visionPosition.Y > -4128)
                    visionPosition.Y -= visionSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }
            else if (keyboard.IsKeyDown(Keys.W))
            {
                if (visionPosition.Y < 32)
                    visionPosition.Y += visionSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
            }

            if (keyboard.IsKeyDown(Keys.Escape))
            {
                DotEGame.Instance.States.Remove(this);
            }

            switch (_turnState)
            {
                case TurnState.Move1:
                case TurnState.Move2:
                case TurnState.Move3:
                    {
                        var happens = FullMove(keyboard);
                        if (happens)
                        {
                            switch (_turnState)
                            {
                                case TurnState.Move1: _turnState = TurnState.Move2; break;
                                case TurnState.Move2: _turnState = TurnState.Move3; break;
                                case TurnState.Move3: _turnState = TurnState.Attack; break;
                            }
                        }
                        else
                        {
                            happens = Attack(keyboard);
                            if (happens)
                            {
                                _turnState = TurnState.Enemy;
                            }
                        }
                    }
                    break;
                case TurnState.Attack:
                    {
                        // You can only attack, if it is a move it will go enemy
                        // TODO: if no enemies around
                        // TODO: escape route
                        var happens = Attack(keyboard);
                        if (happens)
                        {
                            _turnState = TurnState.Enemy;
                        }
                        // TODO, what if you are trapped? guess attack, figure there are other things...
                    }
                    break;
                case TurnState.Enemy:
                    {
                        // TODO: enemy attacks
                        _turnState = TurnState.Move1;
                    }
                    break;
            }

            if (_prev_keyboard.IsKeyUp(Keys.Q) && keyboard.IsKeyDown(Keys.Q))
            {
                visionPosition.X = -_player.X * 32 + 32 * 13;
                visionPosition.Y = -_player.Y * 32 + 32 * 6;
            }

            _prev_keyboard = keyboard;
        }

Then the last additional code is simple to understand:

        private bool Attack(KeyboardState keyboard)
        {
            return false;
            //throw new NotImplementedException();
        }

        private bool Attack(KeyboardState keyboard, int pos_x, int pos_y)
        {
            throw new NotImplementedException();
        }

        private bool FullMove(KeyboardState keyboard)
        {
            // Potential TODO: wrap helpers like seeing if a tile is in vision to minimise if statements
            _rec_entity_pos.X = (_player.X * 32 - 2) + (int)visionPosition.X;
            _rec_entity_pos.Y = (_player.Y * 32 - 2) + (int)visionPosition.Y;
            if ((_rec_entity_pos.X > 0) && (_rec_entity_pos.X < 800) && // player on scree, potential TODO above would also need borders
                (_rec_entity_pos.Y > 0) && (_rec_entity_pos.Y < 600))
            {
                // TODO: make the movements "checks" open up for entity updates, needs to see entities AND player position
                if (_prev_keyboard.IsKeyUp(Keys.Up) && keyboard.IsKeyDown(Keys.Up))
                {
                    if ((_game_board[_player.X, _player.Y - 1] != 0) && PositionClear(_player.X, _player.Y - 1))
                    {
                        _player.Y -= 1;
                    }
                    else
                    {
                        //TODO: attack enemy
                        _turnState = TurnState.Enemy;
                    }
                }
                else if (_prev_keyboard.IsKeyUp(Keys.Left) && keyboard.IsKeyDown(Keys.Left))
                {
                    if ((_game_board[_player.X - 1, _player.Y] != 0) && PositionClear(_player.X - 1, _player.Y))
                    {
                        _player.X -= 1;
                    }
                    else
                    {
                        //TODO: attack enemy
                        _turnState = TurnState.Enemy;
                    }
                }
                else if (_prev_keyboard.IsKeyUp(Keys.Right) && keyboard.IsKeyDown(Keys.Right))
                {
                    if ((_game_board[_player.X + 1, _player.Y] != 0) && PositionClear(_player.X + 1, _player.Y))
                    {
                        _player.X += 1;
                    }
                    else
                    {
                        //TODO: attack enemy
                        _turnState = TurnState.Enemy;
                    }
                }
                else if (_prev_keyboard.IsKeyUp(Keys.Down) && keyboard.IsKeyDown(Keys.Down))
                {
                    if ((_game_board[_player.X, _player.Y + 1] != 0) && PositionClear(_player.X, _player.Y + 1))
                    {
                        _player.Y += 1;
                    }
                    else
                    {
                        //TODO: attack enemy
                        _turnState = TurnState.Enemy;
                    }
                }
            }

            return false;
        }


        private bool PositionClear(int x, int y)
        {
            for (int i = 0; i < _game_entities.Count; i++)
            {
                if ((_game_entities[i].Alive))
                    if ((_game_entities[i].X == x) && (_game_entities[i].Y == y)) return false;
            }

            return true;
        }

This was all to get the play state working, though, that is without the actual play.

Beginning Play

I've thought this trough, and as part of that believe it would be better to stick to a single visual style. We used certain pixel art for the player, and a different style for the grass. Hence, initially we make the grass fit the player art for ourselves.

Grass

Secondly, we step into the map vision, this needs to not have shading and accents. This way it can look decent next to the pixelated art within the rest of the game.

Simplified map

Finally, we start to create the hound. We don't need to stick to the temporary tile, do we? Fitting how we made the character we create the hound for ourselves.

New temporary hound

Despite the fact that the hound isn't complete (and that I can't get the proportions correct yet), it adds more comfort to look at the game. Now that it looks slightly better for me, let us begin wrapping more around the code related to the turns system we used.

Firstly, making the player movement and attacking work out. Take note, this isn't completely balanced or logical yet. It needs work.


        public enum TurnState
        {
            Move1,
            Move2,
            Move3,
            Attack,
            Enemy
        }
   ...
                    case TurnState.Move3:
                        {
                            var happens = FullMove(keyboard);
							if (happens != MovePos.Didnt)
                            {
                                switch (_turnState)
                                {
                                    case TurnState.Move1: _turnState = TurnState.Move2; break;
                                    case TurnState.Move2: _turnState = TurnState.Move3; break;
                                    case TurnState.Move3: _turnState = TurnState.Attack; break;
                                }
                            }
                            else
                            {
                                happens = Attack(keyboard);
								if (happens != MovePos.Didnt)
                                {
                                    _turnState = TurnState.Enemy;
                                }
                            }
                        }
                        break;
   ...
		private MovePos Attack(KeyboardState keyboard)
        {
			int x = _player.X;
			int y = _player.Y;

			if (keyboard.IsKeyDown(Keys.Left))
			{
				x -= 1;
			}
			else if (keyboard.IsKeyDown(Keys.Right))
			{
				x += 1;
			}
			else if (keyboard.IsKeyDown(Keys.Up))
			{
				y -= 1;
			}
			else if (keyboard.IsKeyDown(Keys.Down))
			{
				y += 1;
			}

			for (int i = 0; i < _game_entities.Count; i++)
			{
				if (_game_entities[i].X == x && _game_entities[i].Y == y)
				{
					var target = _game_entities[i];
					// Player Normal Attack
					if ((int)D20.Roll() + _player.StatDexterity > target.StatDexterity)
					{
						target.Damage(_player.StatStrength / 2);
					}

					return MovePos.Down;
				}
			}

			return MovePos.Didnt;
        }

As you may tell above, this isn't completely working out. I am looking around for a niggling bug that I cannot even work out the words for. Take note, I simply made it a simple attack roll, and similarly simple damage. This does not work perfectly, however we can now kill hounds. I stopped here since I need to work out the mechanics I will use for attacking using a D20. Especially, which you will have noticed yourself, it does not pay attention to crit success or failure yet.

Secondly, we can start the hounds actions. He should move every now and then, if the player is within the range of the hound it should attack the player. Note, this we use the idea of timing to help us. Things like 80% chance to move towards the player, use the D20 to see if it is moving normally (I happened to choose 2 for now) and if it is rushing it moves 3 towards the player.

The code within the BehaviorHound's function UpdateMe shows where I am beginning the logic for the turns:


            if (_agro)
            {
                // Move closer to player and attack

                // Distance
                int dist = 2;
                var roll = D20.Roll();
                if ((int)roll >= 18)
                {
                    dist += 1;
                }

                // Path
                //  - options : [up, down, left, right]
                var options = new int[] { 0, 0, 0, 0 };
                for (int i = 0; i < dist; i++)
                {
                    // Check which spot is closer
                    var vl = me.X - 1;
                    var vr = me.X + 1;
                    var vu = me.Y - 1;
                    var vd = me.Y + 1;
                    if (vl > 0)
                    {

                    }
                    else options[2] = -1;
                    if (vr < 128 - 1)
                    {

                    }
                    else options[3] = -1;
                    
                }
            }

Then, if the hound is in attack range of the player it will attack using the D&D style of attacking (as in some will miss, some might have crits, others would be normal). This should be a simple system for balance and adjustments. I just haven't worked out how this will all fit together and it was, therefore, the stopping point for us.

Don't mistake that as a negative answer, it would just be better to take some time to think it through. After we have thought of some good ideas we can step back in and finish off the starting combat system for ourselves.

It also brings the thoughts:

  • Should we show levels of everything in game? I figure it would make more sense, this brings quite a few adjustments to what we have already
  • How should we manage the levelling? I figure every kill should give some experience, similarly every proper action should bring some experience. Consider experience potions, puzzles that you can do to get experience, "training" NPCs in game that can train you into certain moves?
  • Inventory? How shall we manage the inventory of the player, and similarly what about the other entities?

Conclusion Today

I wanted to move this to a slightly more intelligent system for us. That way we can update it more regularly, and it should be easier to manage the new things we add in for ourselves.

It is also a good rule to stick to a single visual style for ourselves, not constantly swapping like I did before. I wanted it to be quick and easy for us. However, now it is better to stick to a certain style for ourselves.

As you can tell, this needed quite a bit to finally be "playable", it still doesn't have an end game. Next time it will be strong consideration of the thoughts written down above. We will also start the first new layer in reality for ourselves.

There will be a portal the player can choose to take, it will be one way. They can move into a certain dimension there, then need to fight new enemies, can get new items and skills. We will leave it there for that simple increase in what we do in this.

Facebook Comments