using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using Coroutine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Media; using MLEM.Cameras; using MLEM.Extended.Extensions; using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting.Codes; using MLEM.Misc; using MLEM.Startup; using MLEM.Textures; using MLEM.Ui; using MLEM.Ui.Elements; using MonoGame.Extended; using MonoGame.Extended.Tiled; using Newtonsoft.Json; using ColorHelper = MLEM.Extensions.ColorHelper; namespace GreatSpringGameJam { public class GameImpl : MlemGame { public static GameImpl Instance { get; private set; } private static readonly JsonSerializer Serializer = new(); public Map Map { get; private set; } public Player Player { get; private set; } public Camera Camera { get; private set; } public bool IsInCutscene { get; private set; } public Dictionary Scores { get; private set; } = new(); private ContentManager mapLoader; public GameImpl() { Instance = this; this.IsMouseVisible = true; } protected override void LoadContent() { this.GraphicsDeviceManager.PreferredBackBufferWidth = 1280; this.GraphicsDeviceManager.PreferredBackBufferHeight = 720; this.GraphicsDeviceManager.ApplyChanges(); base.LoadContent(); this.UiSystem.Style.Font = new GenericSpriteFont(LoadContent("Fonts/Regular")); this.UiSystem.Style.TextScale = 0.075F; this.UiSystem.Style.TextColor = ColorHelper.FromHexRgb(0xebd5bd); this.UiSystem.Style.ButtonTexture = new NinePatch(this.SpriteBatch.GenerateSquareTexture(ColorHelper.FromHexRgb(0x594e6f) * 0.35F), 0); this.UiSystem.Style.ActionSound = new SoundEffectInfo(LoadContent("Sounds/ButtonPress")); this.UiSystem.AutoScaleWithScreen = true; this.UiSystem.Controls.HandleKeyboard = false; this.UiSystem.Controls.HandleGamepad = false; this.UiSystem.Controls.HandleTouch = false; this.UiSystem.TextFormatter.AddImage("Flower", Entity.StuffTexture[0, 1]); this.UiSystem.TextFormatter.AddImage("Gnome", Entity.StuffTexture[1, 1]); this.UiSystem.TextFormatter.AddImage("Snow", Entity.StuffTexture[2, 1]); this.InputHandler.HandleKeyboardRepeats = false; this.mapLoader = new ContentManager(this.Services, this.Content.RootDirectory); var scores = GetScoreFile(); if (scores.Exists) { using var reader = new JsonTextReader(scores.OpenText()); this.Scores = Serializer.Deserialize>(reader); } this.Camera = new Camera(this.GraphicsDevice) { AutoScaleWithScreen = true, Scale = 4 }; // open the main menu var group = new Group(Anchor.TopLeft, Vector2.One, false); group.AddChild(new Image(Anchor.TopCenter, new Vector2(0.45F, -0.5F), Entity.StuffTexture[0, 2, 4, 2]) { OnUpdated = (e, time) => e.PositionOffset = new Vector2(0, 50 + MathF.Sin((float) time.TotalGameTime.TotalSeconds * 2) * 5) }); group.AddChild(new Paragraph(Anchor.BottomCenter, 1, "Created by Ellpeck for the Great Spring Game Jam: 2021", true) { TextScale = 0.06F }); group.AddChild(new Image(Anchor.TopRight, new Vector2(60), i => Entity.StuffTexture[3 + (MediaPlayer.IsMuted ? 1 : 0), 1]) { PositionOffset = new Vector2(4), CanBeMoused = true, OnPressed = e => MediaPlayer.IsMuted = !MediaPlayer.IsMuted }); var buttons = group.AddChild(new Group(Anchor.BottomCenter, new Vector2(500)) {PositionOffset = new Vector2(0, 125)}); buttons.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 60), " Play ") { OnPressed = e => { if (!this.IsInCutscene) this.StartLevel("Overworld", Color.White); } }); buttons.AddChild(new VerticalSpace(10)); buttons.AddChild(new Button(Anchor.AutoLeft, new Vector2(245, 50), "View on itch") { OnPressed = e => Process.Start(new ProcessStartInfo("https://ellpeck.itch.io/a-breath-of-spring-air") {UseShellExecute = true}) }); buttons.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(245, 50), "View source") { PositionOffset = new Vector2(10, 0), OnPressed = e => Process.Start(new ProcessStartInfo("https://git.ellpeck.de/Ellpeck/GreatSpringGameJam") {UseShellExecute = true}) }); this.UiSystem.Add("MainMenu", group); MediaPlayer.IsRepeating = true; MediaPlayer.Play(LoadContent("Songs/SpringPlains")); } protected override void DoUpdate(GameTime gameTime) { base.DoUpdate(gameTime); if (this.Map != null) { this.Map.Update(gameTime); if (this.Player != null && !this.IsInCutscene) this.FocusCameraOnPlayer(); } } protected override void DoDraw(GameTime gameTime) { this.GraphicsDevice.Clear(ColorHelper.FromHexRgb(0x729cd4)); base.DoDraw(gameTime); if (this.Map != null) this.Map.Draw(gameTime, this.SpriteBatch, this.Camera); } public void Finish() { IEnumerable Impl() { // scroll up var offset = 0F; while (offset < 1) { this.Camera.LookingPosition -= new Vector2(0, offset) * this.Map.TileSize; offset += 0.01F; yield return new Wait(CoroutineEvents.Update); } // calculate score var snowRemoved = this.Map.TotalSnow - this.Map.GetTotalTiles("Snow") / 2; var plantsGrown = this.Map.TotalSeeds - this.Map.GetTotalTiles("Seed"); var gnomesCollected = this.Map.TotalGnomes - this.Map.GetEntities().Count(); // snow removal is less important, gnomes are more important var completion = (snowRemoved / 3F + plantsGrown + gnomesCollected * 10F) / (this.Map.TotalSnow / 3F + this.Map.TotalSeeds + this.Map.TotalGnomes * 10F); // display end of level info var group = new Group(Anchor.Center, new Vector2(0.5F), false); this.UiSystem.Add("EndCard", group); group.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Level Completed!", true) {TextScale = 0.125F}); group.AddChild(new VerticalSpace(15)); yield return new Wait(1); group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $" Snow Removed: {snowRemoved} / {this.Map.TotalSnow}", true)); yield return new Wait(1); group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $" Plants Watered: {plantsGrown} / {this.Map.TotalSeeds}", true)); yield return new Wait(1); group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $" Gnomes Collected: {gnomesCollected} / {this.Map.TotalGnomes}", true)); yield return new Wait(1); // display score var completionCounter = 0; group.AddChild(new VerticalSpace(10)); const string scoreText = "Score: {0}%"; var score = group.AddChild(new Paragraph(Anchor.AutoCenter, 1, string.Format(scoreText, 0), true) {TextScale = 0.1F}); while (completionCounter < completion * 100) { completionCounter++; score.Text = string.Format(scoreText, completionCounter); yield return new Wait(0.03F); } // update highest score data if (!this.Scores.TryGetValue(this.Map.Name, out var lastCompletion) || lastCompletion < completionCounter) this.Scores[this.Map.Name] = completionCounter; using (var writer = GetScoreFile().CreateText()) Serializer.Serialize(writer, this.Scores); yield return new Wait(3); // fade out and move to overworld foreach (var wait in this.FadeAndStartLevel("Overworld", Color.White)) yield return wait; } FadeOutSong(); this.IsInCutscene = true; CoroutineHandler.Start(Impl()); } public void StartLevel(string name, Color fadeColor) { FadeOutSong(); this.IsInCutscene = true; CoroutineHandler.Start(this.FadeAndStartLevel(name, fadeColor)); } private IEnumerable FadeAndStartLevel(string mapName, Color fadeColor) { var fadeOverlay = new Group(Anchor.TopLeft, Vector2.One, false) { OnDrawn = (e, time, batch, a) => batch.FillRectangle(e.DisplayArea.ToExtended(), fadeColor * a), DrawAlpha = 0 }; this.UiSystem.Add("Fade", fadeOverlay); while (fadeOverlay.DrawAlpha < 1) { fadeOverlay.DrawAlpha += 0.04F; yield return new Wait(CoroutineEvents.Update); } yield return new Wait(0.25F); this.UiSystem.Remove("EndCard"); this.UiSystem.Remove("MainMenu"); // load next map this.mapLoader.Unload(); var map = new Map(this.mapLoader.Load($"Maps/{mapName}"), mapName); var spawnPoint = map.SpawnPoint.ToVector2(); if (mapName == "Overworld" && this.Map != null && this.Map.Name != mapName) spawnPoint = (map.GetLevelEntrances().First(e => e.Name == this.Map.Name).Position / map.TileSize).FloorCopy(); this.Player = new Player(map, spawnPoint); this.Map = map; this.Map.AddEntity(this.Player); this.FocusCameraOnPlayer(1); // fade back in yield return new Wait(0.25F); while (fadeOverlay.DrawAlpha > 0) { fadeOverlay.DrawAlpha -= 0.04F; yield return new Wait(CoroutineEvents.Update); } this.UiSystem.Remove(fadeOverlay.Root.Name); this.IsInCutscene = false; MediaPlayer.Play(LoadContent("Songs/" + (mapName == "Overworld" ? "SummerForest" : "SpringPlains"))); } private void FocusCameraOnPlayer(float speed = 0.25F) { this.Camera.LookingPosition = Vector2.Lerp(this.Camera.LookingPosition, (this.Player.Position + Vector2.One / 2) * this.Map.TileSize, speed); this.Camera.ConstrainWorldBounds(Vector2.Zero, this.Map.SizeInPixels.ToVector2()); } private static void FadeOutSong() { static IEnumerable Impl() { while (MediaPlayer.Volume > 0) { MediaPlayer.Volume -= 0.05F; yield return new Wait(CoroutineEvents.Update); } MediaPlayer.Stop(); MediaPlayer.Volume = 1; } CoroutineHandler.Start(Impl()); } private static FileInfo GetScoreFile() { var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); return new FileInfo(Path.Combine(appData, $"{Assembly.GetExecutingAssembly().GetName().Name}.dat")); } } }