2021-03-13 02:33:02 +01:00
using System ;
2021-03-08 19:08:41 +01:00
using System.Collections.Generic ;
2021-03-13 20:00:34 +01:00
using System.Diagnostics ;
2021-03-13 02:33:02 +01:00
using System.IO ;
2021-03-09 23:21:52 +01:00
using System.Linq ;
2021-03-13 02:33:02 +01:00
using System.Reflection ;
2021-03-08 19:08:41 +01:00
using Coroutine ;
2021-03-07 22:23:51 +01:00
using Microsoft.Xna.Framework ;
2021-03-17 22:57:20 +01:00
using Microsoft.Xna.Framework.Audio ;
2021-03-09 18:29:07 +01:00
using Microsoft.Xna.Framework.Content ;
2021-03-08 19:08:41 +01:00
using Microsoft.Xna.Framework.Graphics ;
2021-03-18 20:04:47 +01:00
using Microsoft.Xna.Framework.Media ;
2021-03-07 22:23:51 +01:00
using MLEM.Cameras ;
2021-03-09 18:29:07 +01:00
using MLEM.Extended.Extensions ;
2021-03-13 02:33:02 +01:00
using MLEM.Extensions ;
2021-03-08 19:08:41 +01:00
using MLEM.Font ;
2021-03-09 20:13:26 +01:00
using MLEM.Formatting.Codes ;
2021-03-17 22:57:20 +01:00
using MLEM.Misc ;
2021-03-07 22:23:51 +01:00
using MLEM.Startup ;
2021-03-13 20:00:34 +01:00
using MLEM.Textures ;
2021-03-08 19:08:41 +01:00
using MLEM.Ui ;
using MLEM.Ui.Elements ;
2021-03-09 18:29:07 +01:00
using MonoGame.Extended ;
using MonoGame.Extended.Tiled ;
2021-03-13 02:33:02 +01:00
using Newtonsoft.Json ;
2021-03-09 18:29:07 +01:00
using ColorHelper = MLEM . Extensions . ColorHelper ;
2021-03-07 22:23:51 +01:00
namespace GreatSpringGameJam {
public class GameImpl : MlemGame {
public static GameImpl Instance { get ; private set ; }
2021-03-13 02:33:02 +01:00
private static readonly JsonSerializer Serializer = new ( ) ;
2021-03-07 22:23:51 +01:00
public Map Map { get ; private set ; }
public Player Player { get ; private set ; }
2021-03-08 02:37:40 +01:00
public Camera Camera { get ; private set ; }
2021-03-09 18:29:07 +01:00
public bool IsInCutscene { get ; private set ; }
2021-03-13 02:33:02 +01:00
public Dictionary < string , int > Scores { get ; private set ; } = new ( ) ;
2021-03-09 18:29:07 +01:00
private ContentManager mapLoader ;
2021-03-07 22:23:51 +01:00
public GameImpl ( ) {
Instance = this ;
2021-03-08 02:37:40 +01:00
this . IsMouseVisible = true ;
2021-03-07 22:23:51 +01:00
}
protected override void LoadContent ( ) {
this . GraphicsDeviceManager . PreferredBackBufferWidth = 1280 ;
this . GraphicsDeviceManager . PreferredBackBufferHeight = 720 ;
this . GraphicsDeviceManager . ApplyChanges ( ) ;
base . LoadContent ( ) ;
2021-03-08 19:08:41 +01:00
this . UiSystem . Style . Font = new GenericSpriteFont ( LoadContent < SpriteFont > ( "Fonts/Regular" ) ) ;
this . UiSystem . Style . TextScale = 0.075F ;
2021-03-09 18:29:07 +01:00
this . UiSystem . Style . TextColor = ColorHelper . FromHexRgb ( 0xebd5bd ) ;
2021-03-13 20:00:34 +01:00
this . UiSystem . Style . ButtonTexture = new NinePatch ( this . SpriteBatch . GenerateSquareTexture ( ColorHelper . FromHexRgb ( 0x594e6f ) * 0.35F ) , 0 ) ;
2021-03-17 22:57:20 +01:00
this . UiSystem . Style . ActionSound = new SoundEffectInfo ( LoadContent < SoundEffect > ( "Sounds/ButtonPress" ) ) ;
2021-03-08 19:08:41 +01:00
this . UiSystem . AutoScaleWithScreen = true ;
2021-03-13 20:00:34 +01:00
this . UiSystem . Controls . HandleKeyboard = false ;
this . UiSystem . Controls . HandleGamepad = false ;
this . UiSystem . Controls . HandleTouch = false ;
2021-03-09 20:13:26 +01:00
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 ] ) ;
2021-03-07 22:23:51 +01:00
this . InputHandler . HandleKeyboardRepeats = false ;
2021-03-09 18:29:07 +01:00
this . mapLoader = new ContentManager ( this . Services , this . Content . RootDirectory ) ;
2021-03-13 02:33:02 +01:00
var scores = GetScoreFile ( ) ;
if ( scores . Exists ) {
2021-03-13 20:00:34 +01:00
using var reader = new JsonTextReader ( scores . OpenText ( ) ) ;
this . Scores = Serializer . Deserialize < Dictionary < string , int > > ( reader ) ;
2021-03-13 02:33:02 +01:00
}
2021-03-08 02:37:40 +01:00
this . Camera = new Camera ( this . GraphicsDevice ) {
2021-03-07 22:23:51 +01:00
AutoScaleWithScreen = true ,
2021-03-09 02:37:44 +01:00
Scale = 4
2021-03-07 22:23:51 +01:00
} ;
2021-03-13 20:00:34 +01:00
// 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
} ) ;
2021-03-18 20:04:47 +01:00
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
} ) ;
2021-03-13 20:00:34 +01:00
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 )
2021-03-14 15:02:31 +01:00
this . StartLevel ( "Overworld" , Color . White ) ;
2021-03-13 20:00:34 +01:00
}
} ) ;
buttons . AddChild ( new VerticalSpace ( 10 ) ) ;
2021-03-18 20:40:04 +01:00
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 } )
} ) ;
2021-03-13 20:00:34 +01:00
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 ) ;
2021-03-18 20:04:47 +01:00
MediaPlayer . IsRepeating = true ;
MediaPlayer . Play ( LoadContent < Song > ( "Songs/SpringPlains" ) ) ;
2021-03-07 22:23:51 +01:00
}
protected override void DoUpdate ( GameTime gameTime ) {
base . DoUpdate ( gameTime ) ;
2021-03-13 20:00:34 +01:00
if ( this . Map ! = null ) {
this . Map . Update ( gameTime ) ;
if ( this . Player ! = null & & ! this . IsInCutscene )
this . FocusCameraOnPlayer ( ) ;
}
2021-03-07 22:23:51 +01:00
}
protected override void DoDraw ( GameTime gameTime ) {
2021-03-09 18:29:07 +01:00
this . GraphicsDevice . Clear ( ColorHelper . FromHexRgb ( 0x729cd4 ) ) ;
2021-03-07 22:23:51 +01:00
base . DoDraw ( gameTime ) ;
2021-03-13 20:00:34 +01:00
if ( this . Map ! = null )
this . Map . Draw ( gameTime , this . SpriteBatch , this . Camera ) ;
2021-03-07 22:23:51 +01:00
}
2021-03-08 19:08:41 +01:00
public void Finish ( ) {
IEnumerable < Wait > Impl ( ) {
2021-03-09 18:29:07 +01:00
// scroll up
2021-03-08 19:08:41 +01:00
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 ) ;
}
2021-03-13 02:33:02 +01:00
// calculate score
2021-03-10 02:26:36 +01:00
var snowRemoved = this . Map . TotalSnow - this . Map . GetTotalTiles ( "Snow" ) / 2 ;
2021-03-08 19:08:41 +01:00
var plantsGrown = this . Map . TotalSeeds - this . Map . GetTotalTiles ( "Seed" ) ;
2021-03-09 23:21:52 +01:00
var gnomesCollected = this . Map . TotalGnomes - this . Map . GetEntities < Gnome > ( ) . Count ( ) ;
2021-03-09 20:13:26 +01:00
// 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 ) ;
2021-03-08 19:08:41 +01:00
2021-03-13 02:33:02 +01:00
// display end of level info
2021-03-08 19:08:41 +01:00
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 ) ;
2021-03-09 20:13:26 +01:00
group . AddChild ( new Paragraph ( Anchor . AutoCenter , 1 , $"<i Snow> Snow Removed: {snowRemoved} / {this.Map.TotalSnow}" , true ) ) ;
2021-03-08 19:08:41 +01:00
yield return new Wait ( 1 ) ;
2021-03-09 20:13:26 +01:00
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 ) ) ;
2021-03-08 19:08:41 +01:00
yield return new Wait ( 1 ) ;
2021-03-09 18:29:07 +01:00
// display score
2021-03-08 19:08:41 +01:00
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 ) ;
2021-03-09 02:37:44 +01:00
yield return new Wait ( 0.03F ) ;
2021-03-08 19:08:41 +01:00
}
2021-03-09 18:29:07 +01:00
2021-03-13 02:33:02 +01:00
// 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 ) ;
2021-03-09 18:29:07 +01:00
yield return new Wait ( 3 ) ;
2021-03-12 21:11:56 +01:00
// fade out and move to overworld
foreach ( var wait in this . FadeAndStartLevel ( "Overworld" , Color . White ) )
2021-03-09 18:29:07 +01:00
yield return wait ;
2021-03-08 19:08:41 +01:00
}
2021-03-18 20:04:47 +01:00
FadeOutSong ( ) ;
2021-03-09 18:29:07 +01:00
this . IsInCutscene = true ;
2021-03-08 19:08:41 +01:00
CoroutineHandler . Start ( Impl ( ) ) ;
}
2021-03-13 02:33:02 +01:00
public void StartLevel ( string name , Color fadeColor ) {
2021-03-18 20:04:47 +01:00
FadeOutSong ( ) ;
2021-03-09 18:29:07 +01:00
this . IsInCutscene = true ;
2021-03-13 02:33:02 +01:00
CoroutineHandler . Start ( this . FadeAndStartLevel ( name , fadeColor ) ) ;
2021-03-09 18:29:07 +01:00
}
2021-03-13 20:00:34 +01:00
private IEnumerable < Wait > FadeAndStartLevel ( string mapName , Color fadeColor ) {
2021-03-09 18:29:07 +01:00
var fadeOverlay = new Group ( Anchor . TopLeft , Vector2 . One , false ) {
2021-03-13 02:33:02 +01:00
OnDrawn = ( e , time , batch , a ) = > batch . FillRectangle ( e . DisplayArea . ToExtended ( ) , fadeColor * a ) ,
2021-03-09 18:29:07 +01:00
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" ) ;
2021-03-13 20:00:34 +01:00
this . UiSystem . Remove ( "MainMenu" ) ;
2021-03-09 18:29:07 +01:00
2021-03-13 20:00:34 +01:00
// 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
2021-03-09 18:29:07 +01:00
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 ;
2021-03-18 20:04:47 +01:00
MediaPlayer . Play ( LoadContent < Song > ( "Songs/" + ( mapName = = "Overworld" ? "SummerForest" : "SpringPlains" ) ) ) ;
2021-03-09 18:29:07 +01:00
}
2021-03-12 21:11:56 +01:00
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 ( ) ) ;
}
2021-03-18 20:04:47 +01:00
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 ( ) ) ;
}
2021-03-13 02:33:02 +01:00
private static FileInfo GetScoreFile ( ) {
var appData = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ;
return new FileInfo ( Path . Combine ( appData , $"{Assembly.GetExecutingAssembly().GetName().Name}.dat" ) ) ;
}
2021-03-07 22:23:51 +01:00
}
}