diff --git a/O21.Game/Engine/Entities.fs b/O21.Game/Engine/Entities.fs index d2b9327..4796999 100644 --- a/O21.Game/Engine/Entities.fs +++ b/O21.Game/Engine/Entities.fs @@ -53,9 +53,9 @@ type Player = { let newPlayer = { this with Scores = Math.Max(this.Scores + scores, 0) } if this.Oxygen.IsEmpty then PlayerEffect.Die - else + else match CheckCollision level this.Box enemies with - | Collision.OutOfBounds -> PlayerEffect.Update this // TODO[#28]: Level progression + | Collision.OutOfBounds -> PlayerEffect.OutOfBounds this | Collision.CollidesBrick -> PlayerEffect.Die | Collision.CollidesObject _ -> PlayerEffect.Die | Collision.None -> PlayerEffect.Update newPlayer @@ -121,6 +121,7 @@ and OxygenStorage = { and [] PlayerEffect = | Update of Player + | OutOfBounds of Player | Die type Bullet = { diff --git a/O21.Game/Engine/ExternalEffect.fs b/O21.Game/Engine/ExternalEffect.fs index 2209aa6..0ea1212 100644 --- a/O21.Game/Engine/ExternalEffect.fs +++ b/O21.Game/Engine/ExternalEffect.fs @@ -12,4 +12,5 @@ type EntityType = type ExternalEffect = | PlaySound of SoundType + | SwitchLevel of Vector | PlayAnimation of AnimationType * EntityType diff --git a/O21.Game/Engine/GameEngine.fs b/O21.Game/Engine/GameEngine.fs index af341d8..c2e357f 100644 --- a/O21.Game/Engine/GameEngine.fs +++ b/O21.Game/Engine/GameEngine.fs @@ -84,14 +84,20 @@ type GameEngine = { } member this.ChangeLevel(level: Level): GameEngine = - let bombs = level.BombsCoordinates() - |> Array.map (fun point -> - Bomb.Create( - Point(GameRules.LevelWidth / level.LevelMap[0].Length * fst point, - GameRules.LevelHeight / level.LevelMap.Length * snd point))) + let bombs = + level.BombsCoordinates() + |> Array.map (fun point -> + Bomb.Create( + Point(GameRules.LevelWidth / level.LevelMap[0].Length * fst point, + GameRules.LevelHeight / level.LevelMap.Length * snd point))) + + let player = + { this.Player with + TopLeft = this.Player.TopLeft % Point(GameRules.LevelWidth, GameRules.LevelHeight) } { this with CurrentLevel = level + Player = player Bombs = bombs Bullets = Array.empty Fishes = Array.empty @@ -148,6 +154,15 @@ type GameEngine = { static member private ProcessPlayerEffect effect (engine: GameEngine) = match effect with | PlayerEffect.Update player -> { engine with Player = player }, Array.empty + | PlayerEffect.OutOfBounds player -> + let levelDelta = + match player.Box with + | upper when upper.BottomLeft.Y >= GameRules.LevelHeight -> Vector(0, 1) + | lower when lower.TopLeft.Y <= 0 -> Vector(0, -1) + | right when right.TopRight.X >= GameRules.LevelWidth -> Vector(1, 0) + | left when left.TopLeft.X <= 0 -> Vector(-1, 0) + | _ -> Vector(0, 0) + { engine with Player = player }, [| SwitchLevel levelDelta |] | PlayerEffect.Die -> { engine with Player = { engine.Player with diff --git a/O21.Game/Engine/GameField.fs b/O21.Game/Engine/GameField.fs index c35db7c..e1132b6 100644 --- a/O21.Game/Engine/GameField.fs +++ b/O21.Game/Engine/GameField.fs @@ -10,6 +10,10 @@ type Point = static member (+) (Point(x1, y1), Vector(x2, y2)): Point = Point(x1 + x2, y1 + y2) static member (-) (Point(x1, y1), Vector(x2, y2)): Point = Point(x1 - x2, y1 - y2) + + static member (%) (Point(x1, y1), Point(x2, y2)): Point = + let modulo a b = ((a % b) + b) % b + Point(modulo x1 x2, modulo y1 y2) member this.X: int = let (Point(x, _)) = this in x member this.Y: int = let (Point(_, y)) = this in y diff --git a/O21.Game/Engine/GameRules.fs b/O21.Game/Engine/GameRules.fs index 67070fd..1f175c5 100644 --- a/O21.Game/Engine/GameRules.fs +++ b/O21.Game/Engine/GameRules.fs @@ -96,8 +96,8 @@ let NewBulletPosition(playerTopForwardCorner: Point, playerDirection: Horizontal | HorizontalDirection.Left -> playerTopForwardCorner + Vector(-4 - BulletSize.X, 14) | HorizontalDirection.Right -> playerTopForwardCorner + Vector(4, 14) -let StartingLevel = LevelCoordinates(2, 1) -let PlayerStartingPosition = Point(100, 140) +let StartingLevel = LevelCoordinates(1, 1) +let PlayerStartingPosition = Point(200, 140) let LevelSizeInTiles = Vector(50, 25) let NewParticlePosition(playerTopForwardCorner: Point, playerDirection: HorizontalDirection) = diff --git a/O21.Game/HUD.fs b/O21.Game/HUD.fs index 1c442d8..c0b653c 100644 --- a/O21.Game/HUD.fs +++ b/O21.Game/HUD.fs @@ -65,6 +65,7 @@ type HUD = Score = gameEngine.Player.Scores Oxy = gameEngine.Player.OxygenAmount Pause = not gameEngine.IsActive + Level = gameEngine.CurrentLevel.Position } member this.UpdateScore(newScore:int):HUD = diff --git a/O21.Game/Scenes/PlayScene.fs b/O21.Game/Scenes/PlayScene.fs index 47162d6..92410cf 100644 --- a/O21.Game/Scenes/PlayScene.fs +++ b/O21.Game/Scenes/PlayScene.fs @@ -4,6 +4,7 @@ namespace O21.Game.Scenes +open Microsoft.FSharp.Core open O21.Game.Animations open type Raylib_CsLo.Raylib open Raylib_CsLo @@ -94,11 +95,24 @@ type PlayScene = { let game, inputEffects = InputProcessor.ProcessKeys input hud gameEngine let game', updateEffects = game.Update time game', Array.concat [inputEffects; updateEffects] + + static member private UpdateLevelOnRequest (game: GameEngine) (effects: ExternalEffect[]) (levels: Map) = + effects + |> Array.tryPick (fun e -> + match e with + | SwitchLevel delta -> + let dx, dy = delta.X, delta.Y + let newCoords = LevelCoordinates(game.CurrentLevel.Coordinates.X + dx, + game.CurrentLevel.Coordinates.Y + dy) + Some (game.ChangeLevel levels[newCoords]) + | _ -> None) + |> Option.defaultValue game interface IScene with member this.Camera: Camera2D = this.Camera member this.Update(input, time, state) = let game, effects = PlayScene.UpdateGame (input, time) (state.Game, this.HUD) + let game = PlayScene.UpdateLevelOnRequest game effects state.U95Data.Levels let hud = this.HUD.SyncWithGame game let animationHandler = this.AnimationHandler.Update (state, effects) let state = { state with Game = game; Scene = { this with HUD = hud; AnimationHandler = animationHandler } } @@ -128,7 +142,7 @@ type PlayScene = { { this.Window with RenderTargetSize = (GameRules.GameScreenWidth, GameRules.GameScreenHeight) } &this.Camera - DrawTexture(sprites.Background[1], 0, 0, WHITE) + DrawTexture(sprites.Background[game.CurrentLevel.Position], 0, 0, WHITE) let map = game.CurrentLevel.LevelMap for i = 0 to map.Length-1 do for j = 0 to map[i].Length-1 do diff --git a/O21.Game/U95/Level.fs b/O21.Game/U95/Level.fs index 09dab73..acce025 100644 --- a/O21.Game/U95/Level.fs +++ b/O21.Game/U95/Level.fs @@ -17,15 +17,24 @@ type LevelCoordinates = type Level = { LevelMap: MapOfLevel[][] + Position: int + Coordinates: LevelCoordinates } with - static member Empty = { LevelMap = Array.empty } + static member Empty = { + LevelMap = Array.empty + Position = 0 + Coordinates = LevelCoordinates(0, 0) + } - static member private Load(directory:string) (level:int) (part:int): Task = task{ + static member private Load(directory: string) (level: int) (part: int): Task = task{ let parser = Parser(directory) - let level = parser.LoadLevel level part + let coordinates = LevelCoordinates(part, level) + let pos, level = parser.LoadLevel level part return { - LevelMap = level; + LevelMap = level + Position = pos + Coordinates = coordinates } } @@ -50,7 +59,7 @@ type Level = { |> Task.WhenAll return Map.ofArray levelPairs } - + member private this.BombsCoordinatesLazy = lazy ( let mutable coords = Array.empty this.LevelMap @@ -65,3 +74,4 @@ type Level = { ) member this.BombsCoordinates() = this.BombsCoordinatesLazy.Value + diff --git a/O21.Game/U95/Parser.fs b/O21.Game/U95/Parser.fs index c3e641b..1438f91 100644 --- a/O21.Game/U95/Parser.fs +++ b/O21.Game/U95/Parser.fs @@ -13,6 +13,13 @@ type MapOfLevel = | Empty type Parser(directory) = + member private this.getLevelPosition (part: int) = + if part <= 4 then 1 else + if part <= 9 then 2 else + let d1, d2 = (part / 10, part % 10) + let pos = 2 * d1 + 1 + if d2 < 5 then pos else pos + 1 + member private this.readDatFile(level:int) (part:int): string[] = let lines = File.ReadAllLines(Path.Combine(directory, $"U95_{level}_{part}.DAT")) lines @@ -21,7 +28,7 @@ type Parser(directory) = let levelMap = Array.init line.Length (fun i -> let brick = match line[i] with - | t when t>='1' && t <= '9' -> Brick(int line[i] - int '0') + | t when t >='1' && t <= '9' -> Brick(int line[i] - int '0') | 'b' -> Bomb | 'a' -> Bonus | _ -> Empty @@ -36,6 +43,7 @@ type Parser(directory) = ) level - member this.LoadLevel (level:int) (part:int): MapOfLevel[][] = + member this.LoadLevel (level:int) (part:int): int * MapOfLevel[][] = + let pos = this.getLevelPosition part let lines = this.readDatFile level part - this.parseLevel lines + (pos, this.parseLevel lines) diff --git a/O21.Tests/GameEngineTests.fs b/O21.Tests/GameEngineTests.fs index 0d43fd8..21e4b4b 100644 --- a/O21.Tests/GameEngineTests.fs +++ b/O21.Tests/GameEngineTests.fs @@ -28,12 +28,9 @@ let private frameUpN time n gameEngine = let private timeZero = { Total = 0.0; Delta = 0.0f } -let private emptyLevel = { - LevelMap = Array.init GameRules.LevelSizeInTiles.X (fun _ -> - Array.init GameRules.LevelSizeInTiles.Y (fun _ -> MapOfLevel.Empty) - ) -} - +let private emptyLevel = createEmptyLevel + GameRules.LevelSizeInTiles.X + GameRules.LevelSizeInTiles.Y let private newEngine = GameEngine.Create(timeZero, emptyLevel) module Ticks = @@ -130,11 +127,11 @@ module Shooting = module Player = [] let ``Moving towards a brick kills the player``(): unit = - let level = { - LevelMap = [| - [| Empty; Brick 0 |] - |] - } + let level = + { emptyLevel with + LevelMap = [| + [| Empty; Brick 0 |] + |] } let player = { Player.Default with TopLeft = Point(GameRules.BrickSize.X - GameRules.PlayerSize.X, 0) } let player' = player.Update(getEmptyPlayerEnvWithLevel level, 1) Assert.True(match player' with | PlayerEffect.Update _ -> true | _ -> false) @@ -161,7 +158,7 @@ module OxygenSystem = [] let ``Player dies when oxygen amount is empty``(): unit = - let level = { LevelMap = [| [| Empty |] |] } + let level = createEmptyLevel 1 1 let player = Player.Default let player = { player with Player.Oxygen.Amount = 1 } @@ -227,33 +224,29 @@ module Bullets = [] let ``Bullet is destroyed on a brick collision``(): unit = - let level = { - LevelMap = [| - [| Empty; Brick 0 |] - |] - } + let level = + { emptyLevel with + LevelMap = [| + [| Empty; Brick 0 |] + |] } let bullet = defaultBullet let ticksToMove = GameRules.BrickSize.X / GameRules.BulletVelocity Assert.Equal(None, bullet.Update(level, ticksToMove)) [] let ``Bullet doesn't pierce through a brick``(): unit = - let level = { - LevelMap = [| - [| Empty; Brick 0 |] - |] - } + let level = + { emptyLevel with + LevelMap = [| + [| Empty; Brick 0 |] + |] } let bullet = defaultBullet let ticksToMove = GameRules.BrickSize.X * 3 / GameRules.BulletVelocity Assert.Equal(None, bullet.Update(level, ticksToMove)) [] let ``Bullet is destroyed after the expiration of its lifetime``(): unit = - let level = { - LevelMap = [| - [| Empty |] - |] - } + let level = createEmptyLevel 1 1 let bullet = defaultBullet let ticksToMove = GameRules.BulletLifetime Assert.True(bullet.Update(level, ticksToMove).IsSome) @@ -289,11 +282,11 @@ module Bullets = module ScoreSystem = [] let ``Adding scores for hit enemy``(): unit = - let level = { - LevelMap = [| - [| Empty; Bomb |] - |] - } + let level = + { emptyLevel with + LevelMap = [| + [| Empty; Bomb |] + |] } let engine = newEngine.ChangeLevel(level) let engine = @@ -316,20 +309,20 @@ module Geometry = [] let ``Out of bounds check``(): unit = - let level = { LevelMap = Array.empty } + let level = emptyLevel let box1 = { TopLeft = Point(-1, -1); Size = Vector(1, 1) } Assert.Equal(Collision.OutOfBounds, CheckCollision level box1 [||]) - let box2 = { TopLeft = Point(GameRules.LevelWidth, 0); Size = Vector(1, 1) } + let box2 = { TopLeft = Point(GameRules.LevelWidth, GameRules.LevelHeight); Size = Vector(1, 1) } Assert.Equal(Collision.OutOfBounds, CheckCollision level box2 [||]) [] let ``Brick collision check``(): unit = - let level = { - LevelMap = [| - [| Empty; Brick 0 |] - |] - } + let level = + { emptyLevel with + LevelMap = [| + [| Empty; Brick 0 |] + |] } let box1 = { TopLeft = Point(0, 0); Size = Vector(1, 1) } Assert.Equal(Collision.None, CheckCollision level box1 [||]) diff --git a/O21.Tests/Helpers.fs b/O21.Tests/Helpers.fs index 51156da..a411811 100644 --- a/O21.Tests/Helpers.fs +++ b/O21.Tests/Helpers.fs @@ -15,7 +15,7 @@ let getEmptyPlayerEnvWithLevel (level: Level) = BonusColliders = [||] } -let createEmptyLevel width height = { - LevelMap = - Array.create height (Array.create width MapOfLevel.Empty) -} +let createEmptyLevel width height = + { Level.Empty with + LevelMap = + Array.create height (Array.create width MapOfLevel.Empty) }