My entry for the Great Spring Game Jam: A platformer about clearing snow and watering plants to progress https://ellpeck.itch.io/a-breath-of-spring-air
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.
 
 

254 lines
12 KiB

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<string, int> 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<SpriteFont>("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<SoundEffect>("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<Dictionary<string, int>>(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 <l https://ellpeck.de>Ellpeck</l> for the <l https://itch.io/jam/great-spring-game-jam-2021>Great Spring Game Jam: 2021</l>", 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), "<i Gnome> Play <i Flower>") {
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<Song>("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<Wait> 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<Gnome>().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, $"<i Snow> Snow Removed: {snowRemoved} / {this.Map.TotalSnow}", true));
yield return new Wait(1);
group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $"<i Flower> Plants Watered: {plantsGrown} / {this.Map.TotalSeeds}", true));
yield return new Wait(1);
group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $"<i Gnome> 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<Wait> 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<TiledMap>($"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<Song>("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<Wait> 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"));
}
}
}