/// <summary> /// Notifies a character finished training a skill. /// </summary> /// <param name="account"></param> internal void NotifySkillCompletion(CCPCharacter character, QueuedSkill skill) { var notification = new Notification(NotificationCategory.SkillCompletion, character); notification.Description = String.Format("{0} {1} completed.", skill.Skill.Name, Skill.GetRomanForInt(skill.Level)); notification.Behaviour = NotificationBehaviour.Cohabitate; notification.Priority = NotificationPriority.Information; Notify(notification); }
/// <summary> /// Popup a tool tip for the provided skill, above the given point. /// </summary> /// <param name="skill">Skill to display</param> public void Display(QueuedSkill skill, Point pt) { string format = "{0} {1}\n Start{2}\t{3}\n Ends\t{4}"; string skillName = skill.SkillName; string skillLevel = Skill.GetRomanForInt(skill.Level); string skillStart = (skill.Owner.IsTraining ? skill.StartTime.ToLocalTime().ToAbsoluteDateTimeDescription(DateTimeKind.Local) : "Paused"); string skillEnd = (skill.Owner.IsTraining ? skill.EndTime.ToLocalTime().ToAbsoluteDateTimeDescription(DateTimeKind.Local) : "Paused"); string startText = (skill.StartTime < DateTime.UtcNow ? "ed" : "s"); string text = String.Format(CultureConstants.DefaultCulture, format, skillName, skillLevel, startText, skillStart, skillEnd); Display(text, pt); }
/// <summary> /// Gets the rectangle a skill rendes in within a specified rectange. /// </summary> /// <param name="skill">Skill that exists within the queue</param> /// <param name="width">Width of the canvas</param> /// <param name="height">Height of the canvas</param> /// <returns> /// Rectangle representing the area within the visual /// queue the skill occupies. /// </returns> internal static Rectangle GetSkillRect(QueuedSkill skill, int width, int height) { TimeSpan relativeStart; TimeSpan relativeFinish; // Character is training ? we update the timespan if (skill.Owner.IsTraining) { relativeStart = skill.StartTime.Subtract(DateTime.UtcNow); relativeFinish = skill.EndTime.Subtract(DateTime.UtcNow); } // Timespan is stable else { relativeStart = skill.StartTime.Subtract(m_paintTime); relativeFinish = skill.EndTime.Subtract(m_paintTime); } int TotalSeconds = (int)TimeSpan.FromHours(24).TotalSeconds; double Start = Math.Floor((relativeStart.TotalSeconds / TotalSeconds) * width); double Finish = Math.Floor((relativeFinish.TotalSeconds / TotalSeconds) * width); // If the start time is before now set it to zero if (Start < 0) Start = 0; return new Rectangle((int)Start, 0, (int)(Finish - Start), height); }
/// <summary> /// On mouse move, we hide the tooltip. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void lbSkills_MouseMove(object sender, MouseEventArgs e) { for (int i = 0; i < lbSkillsQueue.Items.Count; i++) { // Skip until we found the mouse location Rectangle rect = lbSkillsQueue.GetItemRectangle(i); if (!rect.Contains(e.Location)) continue; // Updates the tooltip m_item = lbSkillsQueue.Items[i] as QueuedSkill; DisplayTooltip(m_item); return; } // If we went so far, we're not over anything. m_lastTooltipItem = null; ttToolTip.Active = false; }
/// <summary> /// On a mouse down event /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void lbSkills_MouseDown(object sender, MouseEventArgs e) { // Retrieve the item at the given point and quit if none int index = lbSkillsQueue.IndexFromPoint(e.X, e.Y); if (index < 0 || index >= lbSkillsQueue.Items.Count) return; // Beware, this last index may actually means a click in the whitespace at the bottom // Let's deal with this special case if (index == lbSkillsQueue.Items.Count - 1) { Rectangle itemRect = lbSkillsQueue.GetItemRectangle(index); if (!itemRect.Contains(e.Location)) return; } // Right click for skills below lv5 : we display a context menu to plan higher levels. m_item = lbSkillsQueue.Items[index] as QueuedSkill; var skill = m_item.Skill; if (skill != null) { if (e.Button == MouseButtons.Right) { // "Show in Skill Explorer" menu item var tmSkillExplorer = new ToolStripMenuItem("Show In Skill Explorer", CommonProperties.Resources.LeadsTo); tmSkillExplorer.Click += tmSkillExplorer_Click; tmSkillExplorer.Tag = skill; // Add to the context menu contextMenuStripPlanPopup.Items.Clear(); contextMenuStripPlanPopup.Items.Add(tmSkillExplorer); if (skill.Level < 5) { // Reset the menu. var tm = new ToolStripMenuItem(String.Format(CultureConstants.DefaultCulture, "Add {0}", skill.Name)); // Build the level options. int nextLevel = Math.Min(5, skill.Level + 1); for (var level = nextLevel; level < 6; level++) { var menuLevel = new ToolStripMenuItem(String.Format(CultureConstants.DefaultCulture, "Level {0} to", Skill.GetRomanForInt(level))); tm.DropDownItems.Add(menuLevel); int level1 = level; Character.Plans.AddTo(menuLevel.DropDownItems, (menuPlanItem, plan) => { menuPlanItem.Click += menuPlanItem_Click; menuPlanItem.Tag = new Pair<Plan, SkillLevel>( plan, new SkillLevel(skill, level1)); }); } // Add to the context menu contextMenuStripPlanPopup.Items.Add(new ToolStripSeparator()); contextMenuStripPlanPopup.Items.Add(tm); } // Display the context menu contextMenuStripPlanPopup.Show((Control) sender, new Point(e.X, e.Y)); return; } } // Non-right click or already lv5, display the tooltip DisplayTooltip(m_item); }
/// <summary> /// Handles the DrawItem event of the lbSkillsQueue control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="System.Windows.Forms.DrawItemEventArgs"/> instance containing the event data.</param> private void lbSkillsQueue_DrawItem(object sender, DrawItemEventArgs e) { if (e.Index < 0) return; m_item = lbSkillsQueue.Items[e.Index] as QueuedSkill; DrawItem(m_item, e); }
/// <summary> /// Gets the tooltip text for the given skill /// </summary> /// <param name="skill"></param> private string GetTooltip(QueuedSkill skill) { if (skill.Skill == null) return String.Empty; int sp = skill.Skill.SkillPoints; int nextLevel = Math.Min(5, skill.Level); double percentCompleted = 0; if (skill.Level == skill.Skill.Level + 1) percentCompleted = skill.Skill.PercentCompleted; if (skill.Level > skill.Skill.Level + 1) sp = skill.CurrentSP; int nextLevelSP = skill.Skill.StaticData.GetPointsRequiredForLevel(nextLevel); int pointsLeft = nextLevelSP - sp; TimeSpan timeSpanFromPoints = skill.Skill.GetTimeSpanForPoints(pointsLeft); string remainingTimeText = timeSpanFromPoints.ToDescriptiveText( DescriptiveTextOptions.IncludeCommas | DescriptiveTextOptions.UppercaseText); if (sp < skill.Skill.StaticData.GetPointsRequiredForLevel(1)) { // Training hasn't got past level 1 yet var untrainedToolTip = new StringBuilder(); untrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Not yet trained to Level I ({0}%)\n", Math.Floor(percentCompleted)); untrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Next level I: {0:#,##0} skill points remaining\n", pointsLeft); untrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time remaining: {0}", remainingTimeText); AddSkillBoilerPlate(untrainedToolTip, skill.Skill); return untrainedToolTip.ToString(); } // So, it's a left click on a skill, we display the tool tip // Currently training skill? if (skill.Skill.IsTraining && percentCompleted != 0) { var partiallyTrainedToolTip = new StringBuilder(); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Partially Completed ({0}%)\n", Math.Floor(percentCompleted)); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training to level {0}: {1:#,##0} skill points remaining\n", Skill.GetRomanForInt(nextLevel), pointsLeft); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time remaining: {0}", remainingTimeText); AddSkillBoilerPlate(partiallyTrainedToolTip, skill.Skill); return partiallyTrainedToolTip.ToString(); } // Currently training skill but next queued level? if (skill.Skill.IsTraining && percentCompleted == 0) { var partiallyTrainedToolTip = new StringBuilder(); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Previous level not yet completed\n"); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Queued to level {0}: {1:#,##0} skill points remaining\n", Skill.GetRomanForInt(nextLevel), pointsLeft); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time to next level: {0}", remainingTimeText); AddSkillBoilerPlate(partiallyTrainedToolTip, skill.Skill); return partiallyTrainedToolTip.ToString(); } // Partially trained skill and not in training? if (skill.Skill.IsPartiallyTrained && !skill.Skill.IsTraining) { var partiallyTrainedToolTip = new StringBuilder(); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Partially Completed ({0}%)\n", Math.Floor(percentCompleted)); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Queued to level {0}: {1:#,##0} skill points remaining\n", Skill.GetRomanForInt(nextLevel), pointsLeft); partiallyTrainedToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time remaining: {0}", remainingTimeText); AddSkillBoilerPlate(partiallyTrainedToolTip, skill.Skill); return partiallyTrainedToolTip.ToString(); } // We've completed all the skill points for the current level if (!skill.Skill.IsPartiallyTrained && skill.Level != 5) { var levelCompleteToolTip = new StringBuilder(); levelCompleteToolTip.AppendFormat(CultureConstants.DefaultCulture, "Completed Level {0}: {1:#,##0}/{2:#,##0}\n", Skill.GetRomanForInt(skill.Level - 1), sp, nextLevelSP); levelCompleteToolTip.AppendFormat(CultureConstants.DefaultCulture, "Queued level {0}: {1:#,##0} skill points required\n", Skill.GetRomanForInt(nextLevel), pointsLeft); levelCompleteToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time to next level: {0}", remainingTimeText); AddSkillBoilerPlate(levelCompleteToolTip, skill.Skill); return levelCompleteToolTip.ToString(); } // Error in calculating SkillPoints var calculationErrorToolTip = new StringBuilder(); calculationErrorToolTip.AppendLine("Partially Trained (Could not calculate all skill details)"); calculationErrorToolTip.AppendFormat(CultureConstants.DefaultCulture, "Next level {0}: {1:#,##0} skill points remaining\n", nextLevel, pointsLeft); calculationErrorToolTip.AppendFormat(CultureConstants.DefaultCulture, "Training time remaining: {0}", remainingTimeText); AddSkillBoilerPlate(calculationErrorToolTip, skill.Skill); return calculationErrorToolTip.ToString(); }
/// <summary> /// Displays the tooltip for the given skill. /// </summary> /// <param name="item"></param> private void DisplayTooltip(QueuedSkill item) { if (ttToolTip.Active && m_lastTooltipItem != null && m_lastTooltipItem == item) return; m_lastTooltipItem = item; ttToolTip.Active = false; ttToolTip.SetToolTip(lbSkillsQueue, GetTooltip(item)); ttToolTip.Active = true; }
/// <summary> /// Check if the queue list has changed /// </summary> public bool QueueHasChanged(QueuedSkill[] queue) { if (m_skillQueue == null) return true; if (queue.Length != m_skillQueue.Length) return true; for (int i = 0; i < queue.Length; i++) { if (queue[i] != m_skillQueue[i]) return true; } return false; }
/// <summary> /// Draws the list item for the given skill /// </summary> /// <param name="skill"></param> /// <param name="e"></param> public void DrawItem(QueuedSkill skill, DrawItemEventArgs e) { Graphics g = e.Graphics; // Draw background if (e.Index%2 == 0) { // Not in training - odd g.FillRectangle(Brushes.LightGray, e.Bounds); } else { // Not in training - even g.FillRectangle(Brushes.White, e.Bounds); } // Measure texts var format = TextFormatFlags.NoPadding | TextFormatFlags.NoClipping; double percentCompleted = 0; int skillPoints = (skill.Skill == null ? 0 : skill.Skill.SkillPoints); int skillPointsToNextLevel = (skill.Skill == null ? 0 : skill.Skill.StaticData.GetPointsRequiredForLevel( Math.Min(skill.Level, 5))); if (skill.Skill != null && skill.Level == skill.Skill.Level + 1) percentCompleted = skill.Skill.PercentCompleted; if (skill.Skill != null && skill.Level > skill.Skill.Level + 1) skillPoints = skill.CurrentSP; string rankText = String.Format(CultureConstants.DefaultCulture, " (Rank {0})", (skill.Skill == null ? 0 : skill.Skill.Rank)); string spText = String.Format(CultureConstants.DefaultCulture, "SP: {0:#,##0}/{1:#,##0}", skillPoints, skillPointsToNextLevel); string levelText = String.Format(CultureConstants.DefaultCulture, "Level {0}", skill.Level); string pctText = String.Format(CultureConstants.DefaultCulture, "{0}% Done", Math.Floor(percentCompleted)); Size skillNameSize = TextRenderer.MeasureText(g, skill.SkillName, m_boldSkillsQueueFont, Size.Empty, format); Size rankTextSize = TextRenderer.MeasureText(g, rankText, m_skillsQueueFont, Size.Empty, format); Size levelTextSize = TextRenderer.MeasureText(g, levelText, m_skillsQueueFont, Size.Empty, format); Size spTextSize = TextRenderer.MeasureText(g, spText, m_skillsQueueFont, Size.Empty, format); Size pctTextSize = TextRenderer.MeasureText(g, pctText, m_skillsQueueFont, Size.Empty, format); // Draw texts Color highlightColor = Color.Black; TextRenderer.DrawText(g, skill.SkillName, m_boldSkillsQueueFont, new Rectangle(e.Bounds.Left + PadLeft, e.Bounds.Top + PadTop, skillNameSize.Width + PadLeft, skillNameSize.Height), highlightColor); TextRenderer.DrawText(g, rankText, m_skillsQueueFont, new Rectangle(e.Bounds.Left + PadLeft + skillNameSize.Width, e.Bounds.Top + PadTop, rankTextSize.Width + PadLeft, rankTextSize.Height), highlightColor); TextRenderer.DrawText(g, spText, m_skillsQueueFont, new Rectangle(e.Bounds.Left + PadLeft, e.Bounds.Top + PadTop + skillNameSize.Height + LineVPad, spTextSize.Width + PadLeft, spTextSize.Height), highlightColor); // Boxes g.DrawRectangle(Pens.Black, new Rectangle(e.Bounds.Right - BoxWidth - PadRight, e.Bounds.Top + PadTop, BoxWidth, BoxHeight)); int levelBoxWidth = (BoxWidth - 4 - 3)/5; for (int level = 1; level <= 5; level++) { var brect = new Rectangle(e.Bounds.Right - BoxWidth - PadRight + 2 + (levelBoxWidth * (level - 1)) + (level - 1), e.Bounds.Top + PadTop + 2, levelBoxWidth, BoxHeight - 3); if (skill.Skill != null && level <= skill.Skill.Level) { g.FillRectangle(Brushes.Black, brect); } else { g.FillRectangle(Brushes.DarkGray, brect); } // Color indicator for a queued level SkillQueue skillQueue = m_ccpCharacter.SkillQueue; if (skill.Skill != null) { Brush brush = (Settings.UI.SafeForWork ? Brushes.Gray : Brushes.RoyalBlue); foreach (QueuedSkill qskill in skillQueue) { if ((!skill.Skill.IsTraining && skill == qskill && level == qskill.Level) || (skill == qskill && level <= qskill.Level && level > skill.Skill.Level && percentCompleted == 0)) g.FillRectangle(brush, brect); // Blinking indicator of skill level in training if (skill.Skill.IsTraining && skill == qskill && level == skill.Level && percentCompleted > 0) { if (m_count == 0) g.FillRectangle(Brushes.White, brect); if (m_count == 1) m_count = -1; m_count++; } } } } // Draw progression bar g.DrawRectangle(Pens.Black, new Rectangle(e.Bounds.Right - BoxWidth - PadRight, e.Bounds.Top + PadTop + BoxHeight + BoxVPad, BoxWidth, LowerBoxHeight)); var pctBarRect = new Rectangle(e.Bounds.Right - BoxWidth - PadRight + 2, e.Bounds.Top + PadTop + BoxHeight + BoxVPad + 2, BoxWidth - 3, LowerBoxHeight - 3); g.FillRectangle(Brushes.DarkGray, pctBarRect); var fillWidth = (int)(pctBarRect.Width * (percentCompleted / 100)); if (fillWidth > 0) { var fillRect = new Rectangle(pctBarRect.X, pctBarRect.Y, fillWidth, pctBarRect.Height); g.FillRectangle(Brushes.Black, fillRect); } // Draw level and percent texts TextRenderer.DrawText(g, levelText, m_skillsQueueFont, new Rectangle( e.Bounds.Right - BoxWidth - PadRight - BoxHPad - levelTextSize.Width, e.Bounds.Top + PadTop, levelTextSize.Width + PadRight, levelTextSize.Height), Color.Black); TextRenderer.DrawText(g, pctText, m_skillsQueueFont, new Rectangle( e.Bounds.Right - BoxWidth - PadRight - BoxHPad - pctTextSize.Width, e.Bounds.Top + PadTop + levelTextSize.Height + LineVPad, pctTextSize.Width + PadRight, pctTextSize.Height), Color.Black); // Draw the queue color bar DrawQueueColorBar(g, e); }
/// <summary> /// Sends a mail alert for a skill completion /// </summary> /// <param name="queueList">Current Skill Queue</param> /// <param name="skill">Skill that has just completed</param> /// <param name="character">Character affected</param> /// <returns></returns> public static bool SendSkillCompletionMail(IList <QueuedSkill> queueList, QueuedSkill skill, Character character) { CCPCharacter ccpCharacter = character as CCPCharacter; // Current character isn't a CCP character, so can't have a Queue. if (ccpCharacter == null) { return(false); } string charName = character.Name; string skillName = skill.SkillName; string skillLevelString = Skill.GetRomanForInt(skill.Level); var skillQueueEndTime = ccpCharacter.SkillQueue.EndTime; bool freeTime = skillQueueEndTime < DateTime.UtcNow.AddHours(24); TimeSpan timeLeft = DateTime.UtcNow.AddHours(24).Subtract(skillQueueEndTime); string timeLeftText = timeLeft.ToDescriptiveText(DescriptiveTextOptions.IncludeCommas, false); // Message's first line StringBuilder body = new StringBuilder(); body.AppendFormat(CultureConstants.DefaultCulture, "{0} has finished training {1} {2}.{3}{3}", charName, skillName, skillLevelString, Environment.NewLine); // Next skills in queue if (queueList[0] != null) { body.AppendFormat(CultureConstants.DefaultCulture, "Next skill{0} in queue:{1}", (queueList.Count > 1 ? "s" : String.Empty), Environment.NewLine); foreach (var qskill in queueList) { body.AppendFormat(CultureConstants.DefaultCulture, "- {0}{1}", qskill, Environment.NewLine); } body.AppendLine(); } else { body.AppendFormat(CultureConstants.DefaultCulture, "Character is not training.{0}{0}", Environment.NewLine); } // Free room in skill queue if (freeTime) { body.AppendFormat(CultureConstants.DefaultCulture, "There is also {0} free room in skill queue.{1}", timeLeftText, Environment.NewLine); } // Short format (also for SMS) if (Settings.Notifications.UseEmailShortFormat) { return(SendMail(Settings.Notifications, String.Format(CultureConstants.DefaultCulture, "[STC] {0} :: {1} {2}", charName, skillName, skillLevelString), body.ToString())); } // Long format if (character.Plans.Count > 0) { body.AppendFormat(CultureConstants.DefaultCulture, "Next skills listed in plans:{0}{0}", Environment.NewLine); } foreach (var plan in character.Plans) { if (plan.Count > 0) { // Print plan name CharacterScratchpad scratchpad = new CharacterScratchpad(character); body.AppendFormat(CultureConstants.DefaultCulture, "{0}:{1}", plan.Name, Environment.NewLine); // Scroll through entries int i = 0; int minDays = 1; foreach (PlanEntry entry in plan) { TimeSpan trainTime = scratchpad.GetTrainingTime(entry.Skill, entry.Level, TrainingOrigin.FromPreviousLevelOrCurrent); // Only print the first three skills, and the very long skills // (first limit is one day, then we add skills duration) if (++i <= 3 || trainTime.Days > minDays) { if (i > 3) { // Print long message once if (minDays == 1) { body.AppendFormat(CultureConstants.DefaultCulture, "{1}Longer skills from {0}:{1}", plan.Name, Environment.NewLine); } minDays = trainTime.Days + minDays; } body.AppendFormat(CultureConstants.DefaultCulture, "\t{0}", entry); // Notes if (entry.Notes != null && entry.Notes.Length > 0) { body.AppendFormat(CultureConstants.DefaultCulture, " ({0})", entry.Notes); } // Training time if (trainTime.Days > 0) { body.AppendFormat(CultureConstants.DefaultCulture, " - {0}d, {1}", trainTime.Days, trainTime); } else { body.AppendFormat(CultureConstants.DefaultCulture, " - {0}", trainTime); } body.AppendLine(); } } body.AppendLine(); } } string subject = String.Format(CultureConstants.DefaultCulture, "{0} has finished training {1} {2}", charName, skillName, skillLevelString); return(SendMail(Settings.Notifications, subject, body.ToString())); }
/// <summary> /// Sends a mail alert for a skill completion /// </summary> /// <param name="queueList">Current Skill Queue</param> /// <param name="skill">Skill that has just completed</param> /// <param name="character">Character affected</param> /// <returns></returns> public static bool SendSkillCompletionMail(IList<QueuedSkill> queueList, QueuedSkill skill, Character character) { CCPCharacter ccpCharacter = character as CCPCharacter; // Current character isn't a CCP character, so can't have a Queue. if (ccpCharacter == null) return false; string charName = character.Name; string skillName = skill.SkillName; string skillLevelString = Skill.GetRomanForInt(skill.Level); var skillQueueEndTime = ccpCharacter.SkillQueue.EndTime; bool freeTime = skillQueueEndTime < DateTime.UtcNow.AddHours(24); TimeSpan timeLeft = DateTime.UtcNow.AddHours(24).Subtract(skillQueueEndTime); string timeLeftText = timeLeft.ToDescriptiveText(DescriptiveTextOptions.IncludeCommas, false); // Message's first line StringBuilder body = new StringBuilder(); body.AppendFormat(CultureConstants.DefaultCulture, "{0} has finished training {1} {2}.{3}{3}", charName, skillName, skillLevelString, Environment.NewLine); // Next skills in queue if (queueList[0] != null) { body.AppendFormat(CultureConstants.DefaultCulture, "Next skill{0} in queue:{1}", (queueList.Count > 1 ? "s" : String.Empty), Environment.NewLine); foreach (var qskill in queueList) { body.AppendFormat(CultureConstants.DefaultCulture, "- {0}{1}", qskill, Environment.NewLine); } body.AppendLine(); } else { body.AppendFormat(CultureConstants.DefaultCulture, "Character is not training.{0}{0}", Environment.NewLine); } // Free room in skill queue if (freeTime) body.AppendFormat(CultureConstants.DefaultCulture, "There is also {0} free room in skill queue.{1}", timeLeftText, Environment.NewLine); // Short format (also for SMS) if (Settings.Notifications.UseEmailShortFormat) { return SendMail(Settings.Notifications, String.Format(CultureConstants.DefaultCulture, "[STC] {0} :: {1} {2}", charName, skillName, skillLevelString), body.ToString()); } // Long format if (character.Plans.Count > 0) body.AppendFormat(CultureConstants.DefaultCulture, "Next skills listed in plans:{0}{0}", Environment.NewLine); foreach (var plan in character.Plans) { if (plan.Count > 0) { // Print plan name CharacterScratchpad scratchpad = new CharacterScratchpad(character); body.AppendFormat(CultureConstants.DefaultCulture, "{0}:{1}", plan.Name, Environment.NewLine); // Scroll through entries int i = 0; int minDays = 1; foreach (PlanEntry entry in plan) { TimeSpan trainTime = scratchpad.GetTrainingTime(entry.Skill, entry.Level, TrainingOrigin.FromPreviousLevelOrCurrent); // Only print the first three skills, and the very long skills // (first limit is one day, then we add skills duration) if (++i <= 3 || trainTime.Days > minDays) { if (i > 3) { // Print long message once if (minDays == 1) { body.AppendFormat(CultureConstants.DefaultCulture, "{1}Longer skills from {0}:{1}", plan.Name, Environment.NewLine); } minDays = trainTime.Days + minDays; } body.AppendFormat(CultureConstants.DefaultCulture, "\t{0}", entry); // Notes if (entry.Notes != null && entry.Notes.Length > 0) { body.AppendFormat(CultureConstants.DefaultCulture, " ({0})", entry.Notes); } // Training time if (trainTime.Days > 0) { body.AppendFormat(CultureConstants.DefaultCulture, " - {0}d, {1}", trainTime.Days, trainTime); } else { body.AppendFormat(CultureConstants.DefaultCulture, " - {0}", trainTime); } body.AppendLine(); } } body.AppendLine(); } } string subject = String.Format(CultureConstants.DefaultCulture, "{0} has finished training {1} {2}", charName, skillName, skillLevelString); return SendMail(Settings.Notifications, subject, body.ToString()); }