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 6 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
14 changes: 11 additions & 3 deletions O21.Game/Animations/AnimationHandler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ 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)
6 changes: 2 additions & 4 deletions O21.Game/Animations/PlayerAnimation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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 +54,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 Seq.exists ((=) AnimationType.Die) animations then
ForNeVeR marked this conversation as resolved.
Show resolved Hide resolved
queue <- this.ExplosionAnimation tick :: queue

{ this with
Expand Down
122 changes: 110 additions & 12 deletions O21.Game/Engine/Entities.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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,47 @@ 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.CalculateScores(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.CollidesBox -> PlayerEffect.Die
| Collision.None -> PlayerEffect.Update newPlayer

member private this.CalculateScores(playerEnv: PlayerEnv) =
ForNeVeR marked this conversation as resolved.
Show resolved Hide resolved
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 isCollides b boxes = (CheckCollision level b boxes).IsCollidesBox
ForNeVeR marked this conversation as resolved.
Show resolved Hide resolved

bullets
|> Array.fold (fun acc b ->
let plus = if isCollides b enemies then GameRules.GiveScoresForBomb else 0 // TODO: Split bomb and fish collision check
let subtract = if isCollides b bonuses then GameRules.SubtractScoresForShotBonus else 0
ForNeVeR marked this conversation as resolved.
Show resolved Hide resolved
acc + plus - subtract) 0

static member Default = {
TopLeft = GameRules.PlayerStartingPosition
Expand All @@ -57,6 +84,7 @@ type Player = {
FreezeTime = 0
Direction = Right
Lives = GameRules.InitialPlayerLives
Scores = 0
Oxygen = OxygenStorage.Default
}
and OxygenStorage = {
Expand Down Expand Up @@ -109,15 +137,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 +160,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.CollidesBox -> 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.CollidesBox -> 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: 2024 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
7 changes: 5 additions & 2 deletions O21.Game/Engine/ExternalEffect.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ 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