Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bombs behavior and score system #214

Merged
merged 8 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion O21.CommandLine.Tests/O21.CommandLine.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
SPDX-FileCopyrightText: 2024 O21 contributors <https://github.com/ForNeVeR/O21>
SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>

SPDX-License-Identifier: MIT
-->
Expand Down
17 changes: 12 additions & 5 deletions O21.Game/Animations/AnimationHandler.fs
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
// SPDX-FileCopyrightText: 2024 O21 contributors <https://github.com/ForNeVeR/O21>
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

namespace O21.Game.Animations

open System
open O21.Game
open O21.Game.Engine
open O21.Game.U95

type AnimationHandler = {
SubmarineAnimation: PlayerAnimation
} with
static member Init(data:U95Data) =
static member Init(data: U95Data) =
{
SubmarineAnimation = PlayerAnimation.Init data
}

member this.Update(state:State, effects: ExternalEffect array) =
{ this with SubmarineAnimation = this.SubmarineAnimation.Update(state, effects) }
member this.Update(state: State, effects: ExternalEffect[]) =
let extractAnim entityType =
effects
|> Array.choose (function
| PlayAnimation (anim, t) when t = entityType -> Some anim
| _ -> None)

let playerAnims = extractAnim EntityType.Player

{ this with SubmarineAnimation = this.SubmarineAnimation.Update(state, playerAnims) }

member this.Draw(state:State) =
this.SubmarineAnimation.Draw(state)
9 changes: 3 additions & 6 deletions O21.Game/Animations/PlayerAnimation.fs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// SPDX-FileCopyrightText: 2024 O21 contributors <https://github.com/ForNeVeR/O21>
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

namespace O21.Game.Animations

open System
open O21.Game
open O21.Game.Engine
open O21.Game.U95
Expand Down Expand Up @@ -44,7 +43,7 @@ type PlayerAnimation = {
CurrentFrame = (0, tick)
}

member this.Update(state: State, effects: ExternalEffect array) =
member this.Update(state: State, animations: AnimationType[]) =
let tick = state.Game.Tick
let player = state.Game.Player
let mutable queue =
Expand All @@ -54,9 +53,7 @@ type PlayerAnimation = {
| None -> this.AnimationQueue.Tail
| Some updated -> updated :: this.AnimationQueue.Tail

if Seq.exists (function
| PlayAnimation a -> a = AnimationType.Die
| _ -> false) effects then
if Array.contains AnimationType.Die animations then
queue <- this.ExplosionAnimation tick :: queue

{ this with
Expand Down
128 changes: 115 additions & 13 deletions O21.Game/Engine/Entities.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: 2024 O21 contributors <https://github.com/ForNeVeR/O21>
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

namespace O21.Game.Engine

open System
open O21.Game.Engine.Environments
open O21.Game.U95
open O21.Game.Engine.Geometry

Expand All @@ -14,6 +16,7 @@ type Player = {
ShotCooldown: int
FreezeTime: int
Lives: int
Scores: int
Oxygen: OxygenStorage
} with

Expand All @@ -32,23 +35,51 @@ type Player = {

member this.Box: Box = { TopLeft = this.TopLeft; Size = GameRules.PlayerSize }

member this.Update(level: Level, timeDelta: int): PlayerEffect =
member this.Update(playerEnv: PlayerEnv, timeDelta: int): PlayerEffect =
let newPlayer =
{ this with
TopLeft = this.TopLeft + this.Velocity * timeDelta
ShotCooldown = max (this.ShotCooldown - timeDelta) 0
FreezeTime = max (this.FreezeTime - timeDelta) 0
Oxygen = this.Oxygen.Update(timeDelta)
}
newPlayer.CheckState(level)
newPlayer.CheckState(playerEnv)

member private this.CheckState(playerEnv: PlayerEnv) =
let level = playerEnv.Level
let enemies = playerEnv.EnemyColliders

let scores = this.CalculateScoreChange(playerEnv)
let newPlayer = { this with Scores = Math.Max(this.Scores + scores, 0) }

member private this.CheckState(level: Level) =
if this.Oxygen.IsEmpty then PlayerEffect.Die
else
match CheckCollision level this.Box with
else
match CheckCollision level this.Box enemies with
| Collision.OutOfBounds -> PlayerEffect.Update this // TODO[#28]: Level progression
| Collision.CollidesBrick -> PlayerEffect.Die
| Collision.None -> PlayerEffect.Update this
| Collision.CollidesObject _ -> PlayerEffect.Die
| Collision.None -> PlayerEffect.Update newPlayer

member private this.CalculateScoreChange(playerEnv: PlayerEnv) =
this.ScoresFromShot(playerEnv)

member private this.ScoresFromShot(playerEnv: PlayerEnv) =
let level = playerEnv.Level
let bullets = playerEnv.BulletColliders
let enemies = playerEnv.EnemyColliders
let bonuses = playerEnv.BonusColliders

let countCollision b boxes =
match CheckCollision level b boxes with
| Collision.CollidesObject count -> count | _ -> 0

(0, bullets)
||> Array.fold (fun acc b ->
let plus =
countCollision b enemies * GameRules.GiveScoresForBomb // TODO: Split bomb and fish collision check
let subtract =
countCollision b bonuses * GameRules.SubtractScoresForShotBonus
acc + plus - subtract)

static member Default = {
TopLeft = GameRules.PlayerStartingPosition
Expand All @@ -57,6 +88,7 @@ type Player = {
FreezeTime = 0
Direction = Right
Lives = GameRules.InitialPlayerLives
Scores = 0
Oxygen = OxygenStorage.Default
}
and OxygenStorage = {
Expand Down Expand Up @@ -109,15 +141,14 @@ type Bullet = {
if timeDelta <= maxTimeToProcessInOneStep then
let newTopLeft =
this.TopLeft +
Vector(this.Direction * this.Velocity.X * timeDelta, this.Velocity.Y * timeDelta)
Vector(this.Velocity.X * timeDelta, this.Velocity.Y * timeDelta)
let newBullet = { this with TopLeft = newTopLeft; Lifetime = newLifetime }

if newLifetime > GameRules.BulletLifetime then None
else
match CheckCollision level newBullet.Box with
| Collision.OutOfBounds -> None
| Collision.CollidesBrick -> None
match CheckCollision level newBullet.Box [||] with
| Collision.None -> Some newBullet
| _ -> None
else
this.Update(level, maxTimeToProcessInOneStep)
|> Option.bind _.Update(level, timeDelta - maxTimeToProcessInOneStep)
Expand All @@ -133,7 +164,78 @@ type Particle = {
this.TopLeft +
Vector(0, VerticalDirection.Up * this.Speed * timeDelta)
let newParticle = { this with TopLeft = newPosition }
match CheckCollision level newParticle.Box with
match CheckCollision level newParticle.Box [||] with
| Collision.OutOfBounds -> None
| Collision.CollidesBrick -> None
| Collision.None -> Some newParticle
| _ -> Some newParticle

[<RequireQualifiedAccess>]
type EnemyEffect<'e> =
| Update of 'e
| PlayerHit of id: int
| Die

type Fish = {
TopLeft: Point
Type: int
Velocity: Vector
Direction: HorizontalDirection
} with
member this.Box = { TopLeft = this.TopLeft; Size = GameRules.FishSizes[this.Type] }

member this.Update(fishEnv: EnemyEnv, timeDelta: int): Fish EnemyEffect = // TODO[#27]: Fish behavior
EnemyEffect.Die

type Bomb = {
Id: int
TopLeft: Point
State: BombState
} with
static member Create(position: Point) =
{
Id = Random.Shared.Next(1, 1000000)
TopLeft = position
State = BombState.Sleep(VerticalTrigger(position.X + GameRules.BombTriggerOffset))
}

member this.Box = { TopLeft = this.TopLeft; Size = GameRules.BombSize }

member this.Update(bombEnv: EnemyEnv, timeDelta: int): Bomb EnemyEffect =
let level = bombEnv.Level
let player = bombEnv.PlayerCollider
let bullets = bombEnv.BulletColliders

let allEntities = Array.append [|player|] bullets

match this.State with
| BombState.Sleep trigger ->
let updated =
if IsTriggered trigger player then
{ this with State = BombState.Active(Vector(0, GameRules.BombVelocity)) }
else
this
match CheckCollision level updated.Box allEntities with
| Collision.CollidesObject _ -> EnemyEffect.PlayerHit this.Id
| Collision.None -> EnemyEffect.Update updated
| _ -> EnemyEffect.Die
| BombState.Active velocity ->
// Check each intermediate position of the bomb for collision:
let maxTimeToProcessInOneStep = GameRules.PlayerSize.Y / Math.Abs(velocity.Y)
if maxTimeToProcessInOneStep <= 0 then failwith "maxTimeToProcessInOneStep <= 0"
if timeDelta <= maxTimeToProcessInOneStep then
let newBomb =
{ this with
TopLeft = this.TopLeft + velocity * timeDelta }
match CheckCollision level this.Box allEntities with
| Collision.CollidesObject _ -> EnemyEffect.PlayerHit this.Id
| Collision.None -> EnemyEffect.Update newBomb
| _ -> EnemyEffect.Die
else
let effect = this.Update(bombEnv, maxTimeToProcessInOneStep)
match effect with
| EnemyEffect.Update newBomb -> newBomb.Update(bombEnv, timeDelta - maxTimeToProcessInOneStep)
| _ -> effect

and [<RequireQualifiedAccess>] BombState =
| Sleep of trigger: Trigger
| Active of velocity: Vector
21 changes: 21 additions & 0 deletions O21.Game/Engine/Environments.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT
module O21.Game.Engine.Environments

open O21.Game.U95

type PlayerEnv = {
Level: Level
BulletColliders: Box[]
EnemyColliders: Box[]
BonusColliders: Box[]
}

type EnemyEnv = {
Level: Level
PlayerCollider: Box
BulletColliders: Box[]
}

type BonusEnv = EnemyEnv
9 changes: 6 additions & 3 deletions O21.Game/Engine/ExternalEffect.fs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// SPDX-FileCopyrightText: 2024 O21 contributors <https://github.com/ForNeVeR/O21>
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

namespace O21.Game.Engine

open O21.Game.Animations
open O21.Game.U95

type EntityType =
| Player
| Enemy of id: int

type ExternalEffect =
| PlaySound of SoundType
| PlayAnimation of AnimationType
| PlayAnimation of AnimationType * EntityType
Loading