Lot of progress today!

Actor Emotes

The previous update added floating combat text in the form of Animation_ActorTalk. This update adds another way for Actors to communicate – Emotes.  These are those little “thought bubbles” that appear above mobs in (among other places) many mobile JRPGs.  They typically communicate state information such as sleeping or stunned.

To support these, I added an Animation_ActorEmote class which displays a little bouncing popup bubble above an Actor for a few seconds.  I’ve also added one Emote type: Aggro; this is triggered/shown whenever an Actor aggros onto a new Actor.  Here’s what it looks like:

You can’t see it in the shot, but the bubbles bounce slightly at random heights, kind of a neat effect.  Also neat is how easily this slotted into the Animation system.  Here’s the entirety of the Animation_ActorEmote class:


    // The set of Emote bubbles that can be displayed above Actors.
    public enum Emote { Aggro };

    /// <summary>
    /// Displays an 'emote bubble' above the actor
    /// TBD-later: not handling case where one actor has multiple emote bubbles.
    /// Ideally, alternate between them in that case.
    /// </summary>
    public class Animation_ActorEmote : Animation
    {
        /// <summary>
        /// Constructor.  Track the actor and emote to display
        /// </summary>
        /// <param name="actor">The actor above whom the emote will display</param>
        /// <param name="emote">The emote to display</param>
        public Animation_ActorEmote(Actor actor, Emote emote)
            : base()
        {
            _actor = actor;
            _emoteBubble = AssetMgr.EmoteBubbles[emote];

            // Start Bouncing
            StartBounce();
        }

        /// <summary>
        /// Starts bouncing the emote bubble
        /// </summary>
        void StartBounce()
        {
            _bounceHeight = 5 + Rand.Next(5);
            _bounceAmount = 0;
            _bounceStartTime = (AnimationMgr.CurrentTime - this.StartTime).TotalMilliseconds;
            _bounceEndTime = _bounceStartTime + _bounceTime;
        }

        /// <summary>
        /// Return true when the animation has completed and should be removed
        /// </summary>
        /// <returns>True when the animation has completed</returns>
        public override bool Completed()
        {
            return (AnimationMgr.CurrentTime - this.StartTime).TotalMilliseconds >= _msToDisplay;
        }

        /// <summary>
        /// Calculates the current height of the bouncing emote bubble.  Restarts the bounce if
        /// we've reached the end of the current bounce.
        /// </summary>
        /// <returns>Height of the current bounce</returns>
        int DoBounce()
        {
            double curTime = (AnimationMgr.CurrentTime - this.StartTime).TotalMilliseconds;
            if (curTime > _bounceEndTime)
                StartBounce();
            float amountBounced = (float)((curTime - _bounceStartTime) / _bounceTime);
            return (int)(Math.Sin(amountBounced * Math.PI) * _bounceHeight);
        }

        /// <summary>
        /// Render the emote bubble
        /// </summary>
        public override void Render()
        {
            // Don't render if player can't see the actor
            if (!Player.Current.ThisTurnVisibleCells.Contains(_actor.Location))
                return;

            // Determine how long we've been bouncing
            int msGoneBy = (int)((AnimationMgr.CurrentTime - this.StartTime).TotalMilliseconds);

            // Get how high we're bouncing
            int bounceHeight = DoBounce();

            // Determine X coordinate
            Point actorMiddleTop = GameState_InDungeon.MapCellToScreenCoords(_actor.Location);
            Point messageCenter = new Point(actorMiddleTop.X - _emoteBubble.Width / 2,
                                            actorMiddleTop.Y - 12 - bounceHeight);

            RenderMgr.DrawSurface(_emoteBubble, messageCenter.X, messageCenter.Y);
        }

        // _actor: The actor above whom this bubble will display
        Actor _actor;

        // _emoteBubble: The bubble to display
        GameSurface _emoteBubble;

        // _msToDisplay: total time to display the emote (in milliseconds)
        static int _msToDisplay = 3000;

        // _bounceHeight: Maximum height of the current bounce
        int _bounceHeight = 0;

        // _bounceAmount: Amount we've bounced in the current bounce so far
        int _bounceAmount = 0;

        // _bounceTime: How often to bounce (1000 ms = once per second)
        static double _bounceTime = 1000;

        // _bounceEndTime: When to stop the current bounce
        double _bounceEndTime;

        // _bounceStartTime: When the current bounce started
        double _bounceStartTime;
    }

Creating a new emote animation is a simple fire-and-forget operation. Here’s the code that is called when a new Aggro target is identified:

        /// <summary>
        /// Called when the Actor has a new Aggro target and we should notify the player
        /// </summary>
        public void NotifyAggro()
        {
            AnimationMgr.AddAnimation(new Animation_ActorEmote(this, Emote.Aggro));
        }

Each Emote has a “chat bubble” specific to it, and in the future it’ll be easy to add new ones (stunned, sleeping, power up, etc). One thing I don’t have yet is the ability to render multiple chat bubbles properly (for now it just overlays them) – ideally it would alternate them…

Experience points and Leveling

A big addition was that of experience points and leveling.  You can see the XP bar in the image above – that fills up as you kill mobs, and when filled up you bump up a new level.  Although only the player can level up for now, I’ve implemented this in the base Actor class –  that’ll allow pets to level up, and NPC party members or even charmed monsters could level up as well over time.

The amount of XP given for killing a monster is currently hardcoded to a set of values that comes from a pretty complicated set of formulas intending to deliver a reasonabe curve; It’s not well thought out yet, and I’ll try to cover that in a separate post later. Here’s the set of values:

        // xpPerMobAtLevel: Experience points given for killing an Actor of level "x"
        public float[] xpPerMobAtLevel = {0, 10.0f,12.5f,12.5f,21.2f,27.9f,36.3f,44.6f,52.9f,61.2f,71.8f,82.6f,93.6f,104.7f,116.0f,130.6f,145.7f,161.3f,177.3f,193.8f,
            210.6f,227.7f,245.2f,263.0f,281.1f,299.5f,318.1f,337.0f,356.1f,375.5f,395.0f,414.7f,434.6f,454.7f,475.0f,495.4f,516.0f,536.7f,
            557.5f,578.5f,599.6f,620.8f,642.2f,663.6f,685.1f,706.8f,728.5f,750.3f,772.3f,794.3f,816.3f};

Killing a monster calls Actor.KilledActor, in which the amount of experience points earned is calculated. This is a combination of the base XP given for the level of the killed actor, augmented by the delta in levels between the killer and the target. Specifically, a 20% bonus or deficit to XP is applied for each difference in level number (minimizing out at 0% XP for monsters more than 5 levels below the player).

        /// <summary>
        /// Get XP for killing the victim
        /// </summary>
        /// <param name="victim">Who we killed</param>
        override public void KilledActor(Actor victim)
        {
            float xpToAdd = xpPerMobAtLevel[victim.ActorLevel];

            // Apply modifier based on player's level; 20% per integer difference between levels up to 100% max
            int levelDelta = this.ActorLevel - victim.ActorLevel;
            if (this.ActorLevel > victim.ActorLevel)
                xpToAdd *= Math.Max(0, (1 - levelDelta * .2f)); // monsters more than 5 levels under us give no XP
            else if (this.ActorLevel < victim.ActorLevel)
                xpToAdd *= (1 - levelDelta * .2f);
            AddExperiencePoints((int)xpToAdd);
        }

Once the number of XP is determined, we call into AddExperiencePoints, where the points are added and we determine if a new level has been reached. We give some stat bonuses which are kind of meaningless right now. This is where we’ll also add any new Skills that the player earns when reaching the new level.

        private void AddExperiencePoints(int pointsToAdd)
        {
            ExperiencePoints += pointsToAdd;
            if (ExperiencePoints >= xpAtEndOfLevel[ActorLevel])
            {
                ExperiencePoints -= (int)xpAtEndOfLevel[ActorLevel];

                ActorLevel++;

                int extraStr = (int)(strIncAtLevel * Rand.NextPercent() * 2);
                int extraDex = (int)(intIncAtLevel * Rand.NextPercent() * 2);
                int extraInt = (int)(dexIncAtLevel * Rand.NextPercent() * 2);

                MessageMgr.AddMessage("You have gained a level!  You are now Level " + ActorLevel, Color.Yellow);

                if (extraDex > 0)
                {
                    MessageMgr.AddMessage("You gained " + extraDex + " Dexterity", Color.Cyan);
                    Dexterity += extraDex;
                }
                if (extraStr > 0)
                {
                    MessageMgr.AddMessage("You gained " + extraStr + " Strength", Color.Cyan);
                    Strength += extraStr;
                }
                if (extraInt > 0)
                {
                    MessageMgr.AddMessage("You gained " + extraInt + " Intelligence", Color.Cyan);
                    Intelligence += extraInt;
                }

                int newHP = CalcHP();
                if (newHP > MaximumHitPoints)
                {
                    MessageMgr.AddMessage("You gained " + (int)(newHP - MaximumHitPoints) + " Health", Color.Cyan);
                    HitPoints = MaximumHitPoints = newHP;
                }

                // Restore all health on level gain
                HitPoints = MaximumHitPoints;

                string msg = "You have gained a level!|You are now Level " + ActorLevel;

                RenderMgr.HiPriMessage(msg);
            }
        }

Regeneration

When the player rests they now regenerate health.  The determination for when regeneration occurs is as follows:

  • Actors regenerate a percentage  of health (eventually determined by race and buffs) per turn that Regen is active.
  • An Actor’s regeneration timer is reset if the actor takes damage OR attacks (et al: e.g. cast)
  • Regeneration is reactived ‘n’ (determined by race and buffs) turns after regen timer is reset.
  • Note that MoBs can regenerate too!  Later, I’ll add AI for certain monsters that flee and regen and turn around when health is high enough again..

At the start of Actor.PerformNextAction, we check to see if we’re in regeneration mode


        virtual public void PerformNextAction()
        {
            // First, check if we should regen health.  TBD: Tie into Buff system when that's present?
            if (HitPoints < MaximumHitPoints && NumTurnsRemainingUntilRegenIsActive == 0)
            {
                HitPoints += (int)(MaximumHitPoints * PercentOfHealthToRegenPerTurn);
            }
            ...
         }

Actor tile direction

Actors now face left or right based on their last move OR when damaged (in which case they face their damager).

Miscellaneous fixes

I also got around to a number of minor fixes, including:

  • Messages weren’t fading properly
  • The player now stops (and cannot start) exploring when an Actor is aggro’ed onto them. They can still tap-to-move though
  • Hitting a closed door now stops exploration. I’m not sure if this is the right decision yet…
  • The main display renders centered around the player a few tiles higher now
  • The map overlay is rendered smaller now (although the overlay is temporary, it was obscuring too much)
  • After dungeon generation, we now do a fixup on doors and remove any that are not logical (e.g. don’t have walls on both sides)

Updated source: http://wanderlinggames.com/files/dungeon/dungeon-3-22-11.zip

Updated executable: http://wanderlinggames.com/files/dungeon/dungeonexe-3-22-11.zip

Advertisements