Skip to content

Commit

Permalink
✨ Add tired of touring moodlet
Browse files Browse the repository at this point in the history
Now when the band performs too many concerts in a row, the character will get a moodlet to cap the points the band gets during a concert.
  • Loading branch information
sleepyfran committed Oct 29, 2023
1 parent eb37293 commit e35137e
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 69 deletions.
4 changes: 4 additions & 0 deletions src/Duets.Cli/Components/Commands/Me.Command.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ module MeCommand =
| MoodletType.JetLagged -> "Jet lagged" |> Styles.warning
| MoodletType.NotInspired ->
"Not inspired" |> Styles.warning
| MoodletType.TiredOfTouring ->
"Tired of touring" |> Styles.warning

let moodletExplanation =
match moodlet.MoodletType with
| MoodletType.JetLagged ->
"You've traveled to a city with a very different timezone, you'll need some time to adjust. You might feel more tired than usual"
| MoodletType.NotInspired ->
"You've been composing too much lately, better take a break! Composing and improving songs while not inspired won't be as effective"
| MoodletType.TiredOfTouring ->
"You've been having too many concerts in the past few days, you need some rest! You won't be able to perform as well as usual"

let moodletExpirationText =
match moodlet.Expiration with
Expand Down
2 changes: 2 additions & 0 deletions src/Duets.Cli/Components/Effect.fs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ let private displayEffect effect =
"You feel a bit confused because of the time difference, you might feel more tired than usual"
| MoodletType.NotInspired ->
"You've been composing too much and you're not feeling inspired anymore. Try waiting a few days..."
| MoodletType.TiredOfTouring ->
"You've had a lot of concerts lately and you're feeling tired. Try waiting a few days..."
|> Styles.warning
|> showMessage)
| ConcertScheduled(_, ScheduledConcert(concert, _)) ->
Expand Down
6 changes: 4 additions & 2 deletions src/Duets.Cli/Text/Concert.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ let playSongLowPerformanceReaction energy reasons points =
| CharacterDrunk -> "you were quite drunk"
| LowPractice -> "you didn't practice the song enough"
| LowSkill -> "you didn't practice at home enough"
| LowQuality -> "the song was not that good")
| LowQuality -> "the song was not that good"
| TooTired -> "you are all too tired of giving concerts")

Styles.Level.bad
$"""Unfortunately it seems like {reasonsText}. You got {points} {Generic.simplePluralOf "point" points}. {energyText}"""
Expand All @@ -124,7 +125,8 @@ let playSongMediumPerformanceReaction reasons points =
| CharacterDrunk -> "being drunk"
| LowPractice -> "not having practiced the song enough"
| LowSkill -> "not having good skills"
| LowQuality -> "the song not being so good")
| LowQuality -> "the song not being so good"
| TooTired -> "being too tired of giving concerts")

Styles.Level.normal
$"""You didn't nail the performance, probably {reasonsText} didn't help. But anyway, you got {points} {Generic.simplePluralOf "point" points}"""
Expand Down
1 change: 1 addition & 0 deletions src/Duets.Cli/Text/Emoji.fs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ let moodlet m =
match m with
| MoodletType.JetLagged -> ":sleepy_face:"
| MoodletType.NotInspired -> ":expressionless_face:"
| MoodletType.TiredOfTouring -> ":minibus:"
1 change: 1 addition & 0 deletions src/Duets.Entities/Types/Moodlet.Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module MoodletTypes =
type MoodletType =
| JetLagged
| NotInspired
| TiredOfTouring

/// Defines a moodlet that can be applied to a character.
type Moodlet =
Expand Down
15 changes: 14 additions & 1 deletion src/Duets.Simulation/Concerts/Live/Live.Common.fs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ type ConcertEventResultReason =
| LowPractice
| LowSkill
| LowQuality
| TooTired

/// Defines the result of an event in the concert.
type ConcertEventResult =
Expand Down Expand Up @@ -241,6 +242,17 @@ let private baseScoreWithReasons state action =
let avgQualities = qualities |> List.average
reasons, avgQualities

let private applyMoodlets state (reasons, score) =
let characterTiredOfTouring =
Queries.Characters.playableCharacterHasMoodlet
state
MoodletType.TiredOfTouring

if characterTiredOfTouring then
reasons @ [ TooTired ], score * 0.4
else
reasons, score

/// Computes the average modifier to apply based on the score rules that are
/// modifiers (value between 0.0 and 1.0) rather than a quality (value between
/// 0 and 100).
Expand Down Expand Up @@ -317,7 +329,8 @@ and private performAction' state ongoingConcert action =
|> Response.addEffects action.Effects

and private ratePerformance state ongoingConcert action =
let qualityReasons, baseScore = baseScoreWithReasons state action
let qualityReasons, baseScore =
baseScoreWithReasons state action |> applyMoodlets state

let multipliers =
if List.isEmpty action.Multipliers then
Expand Down
1 change: 1 addition & 0 deletions src/Duets.Simulation/Duets.Simulation.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<Compile Include="Events\Moodlets\NotInspired.fs" />
<Compile Include="Events\Moodlets\Cleanup.fs" />
<Compile Include="Events\Moodlets\JetLagged.fs" />
<Compile Include="Events\Moodlets\TiredOfTouring.fs" />
<Compile Include="Events\Moodlets\Moodlets.Events.fs" />
<Compile Include="Events\Career.Events.fs" />
<Compile Include="Events\NonInteractiveGame.Events.fs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ let modify state effect =
(fun effect moodlet ->
match moodlet.MoodletType with
| MoodletType.JetLagged -> modifyForJetLagged effect
| MoodletType.NotInspired -> modifyForNotInspired effect)
| MoodletType.NotInspired -> modifyForNotInspired effect
| MoodletType.TiredOfTouring -> effect (* Nothing to apply. *) )
effect

/// Modifies the SongStarted and SongImproved effects to reduce the quality
Expand Down
2 changes: 2 additions & 0 deletions src/Duets.Simulation/Events/Moodlets/Moodlets.Events.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ open Duets.Simulation.Events
/// to the character for another week to slow them down a bit.
let internal run effect =
match effect with
| ConcertFinished(band, _, _) ->
ContinueChain [ TiredOfTouring.applyIfNeeded band.Id ] |> Some
| SongFinished(band, _, _) ->
ContinueChain [ NotInspired.applyIfNeeded band.Id ] |> Some
| TimeAdvanced _ -> ContinueChain [ Cleanup.cleanup ] |> Some
Expand Down
44 changes: 44 additions & 0 deletions src/Duets.Simulation/Events/Moodlets/TiredOfTouring.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Duets.Simulation.Events.Moodlets.TiredOfTouring

open Duets.Common
open Duets.Entities
open Duets.Simulation

let private concertsToCheck = 5

/// Checks if the band has had at least a 7 day break between their last 4
/// concerts. If not, applies the TiredOfTouring moodlet.
let applyIfNeeded bandId state =
let lastFourConcerts =
Queries.Concerts.allPast state bandId
|> Seq.sortBy (fun event ->
let concert = Concert.fromPast event
concert.Date)
|> Seq.truncate concertsToCheck
|> Seq.toList

match lastFourConcerts with
| concerts when concerts.Length = concertsToCheck ->
(*
Check that the span between the first and fourth concert has been of
at least a week.
*)
let firstConcert = concerts |> List.head |> Concert.fromPast
let lastConcert = concerts |> List.last |> Concert.fromPast

let daysInBetween =
Calendar.Query.daysBetween lastConcert.Date firstConcert.Date

let shouldApplyMoodlet = daysInBetween < 7<days>

if shouldApplyMoodlet then
let moodlet =
Character.Moodlets.createFromNow
state
MoodletType.TiredOfTouring
(MoodletExpirationTime.AfterDays 14<days>)

[ Character.Moodlets.apply state moodlet ]
else
[]
| _ -> [] (* Not enough concerts have happened yet, do nothing. *)
65 changes: 57 additions & 8 deletions tests/Simulation.Tests/Concerts/Live.PlaySong.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ let ``playSong lowers result depending on character's drunkenness`` () =
let response =
playSong drunkState dummyOngoingConcert song Energetic

response |> resultFromResponse |> should be (ofCase expectedResult)))
response
|> resultFromResponse
|> should be (ofCase expectedResult)))

[<Test>]
let ``playSong does not decrease points below 0`` () =
Expand All @@ -249,10 +251,13 @@ let ``playSong does not decrease points below 0`` () =

[<Test>]
let ``playSong should add points to the previous count to ongoing concert`` () =
let ongoingConcert = { dummyOngoingConcert with Points = 50<quality> }
let ongoingConcert =
{ dummyOngoingConcert with
Points = 50<quality> }

Generators.Song.finishedGenerator
{ Generators.Song.defaultOptions with PracticeMin = 1 }
{ Generators.Song.defaultOptions with
PracticeMin = 1 }
|> Gen.sample 0 1000
|> List.iter (fun song ->
playSong dummyState ongoingConcert song Energetic
Expand All @@ -262,7 +267,9 @@ let ``playSong should add points to the previous count to ongoing concert`` () =

[<Test>]
let ``playSong does not increase above 100`` () =
let ongoingConcert = { dummyOngoingConcert with Points = 98<quality> }
let ongoingConcert =
{ dummyOngoingConcert with
Points = 98<quality> }

Generators.Song.finishedGenerator Generators.Song.defaultOptions
|> Gen.sample 0 1000
Expand Down Expand Up @@ -318,7 +325,7 @@ let ``playSong should decrease health by 2 points and energy by 5 when performin
|> List.item 0
|> fun effect ->
match effect with
| CharacterAttributeChanged (_, attr, amount) ->
| CharacterAttributeChanged(_, attr, amount) ->
attr |> should equal CharacterAttribute.Health
amount |> should equal (Diff(100, 98))
| _ -> failwith "Effect was not of correct type"
Expand All @@ -327,7 +334,7 @@ let ``playSong should decrease health by 2 points and energy by 5 when performin
|> List.item 1
|> fun effect ->
match effect with
| CharacterAttributeChanged (_, attr, amount) ->
| CharacterAttributeChanged(_, attr, amount) ->
attr |> should equal CharacterAttribute.Energy
amount |> should equal (Diff(100, 95))
| _ -> failwith "Effect was not of correct type"
Expand All @@ -350,7 +357,7 @@ let ``playSong should energy by 3 points when performing normally`` () =
|> List.item 0
|> fun effect ->
match effect with
| CharacterAttributeChanged (_, attr, amount) ->
| CharacterAttributeChanged(_, attr, amount) ->
attr |> should equal CharacterAttribute.Energy
amount |> should equal (Diff(100, 97))
| _ -> failwith "Effect was not of correct type"
Expand All @@ -369,7 +376,49 @@ let ``playSong should energy by 1 point when performing in limited`` () =
|> List.item 0
|> fun effect ->
match effect with
| CharacterAttributeChanged (_, attr, amount) ->
| CharacterAttributeChanged(_, attr, amount) ->
attr |> should equal CharacterAttribute.Energy
amount |> should equal (Diff(100, 99))
| _ -> failwith "Effect was not of correct type"

let private tiredOfTouringMoodlets =
[ Moodlet.create
MoodletType.TiredOfTouring
dummyToday
MoodletExpirationTime.Never ]
|> Set.ofList

[<Test>]
let ``playSong reduces score by 60% if character is too tired of touring`` () =
let state =
dummyState
|> State.Characters.setMoodlets dummyCharacter.Id tiredOfTouringMoodlets

Generators.Song.finishedGenerator
{ Generators.Song.defaultOptions with
LengthRange = 10<minute>, 20<minute> }
|> Gen.sample 0 1000
|> List.iter (fun song ->
let response = playSong state dummyOngoingConcert song Energetic

let reasons =
match response.Result with
| LowPerformance reasons -> reasons
| AveragePerformance reasons -> reasons
| GoodPerformance reasons -> reasons
| _ -> failwith "Unexpected result"

reasons
|> List.filter (function
| TooTired -> true
| _ -> false)
|> should haveLength 1

response
|> ongoingConcertFromResponse
|> Optic.get Lenses.Concerts.Ongoing.points_
|> should be (inRange 0<quality> 10<quality>)

response
|> pointsFromResponse
|> should be (inRange 0<quality> 10<quality>))
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module Duets.Simulation.Tests.Events.Moodlets.JetLagged

open FsUnit
open NUnit.Framework
open Test.Common
open Test.Common.Generators

open Duets.Common
open Duets.Entities
open Duets.Simulation

let private worldMoveEffect prevCity currCity =
let queryPlace cityId =
Queries.World.placesByTypeInCity cityId PlaceTypeIndex.Airport
|> List.head

let prevCityPlace = queryPlace prevCity
let currCityPlace = queryPlace currCity


WorldMoveTo(
Diff((prevCity, prevCityPlace.Id, 0), (currCity, currCityPlace.Id, 0))
)

[<Test>]
let ``tick of WorldMoveTo does not apply any extra effects if the difference in timezones is less than 4 hours``
()
=
[ London, Prague; Madrid, Prague; NewYork, MexicoCity; Sydney, Tokyo ]
|> List.iter (fun (prevCity, currCity) ->
Simulation.tickOne dummyState (worldMoveEffect prevCity currCity)
|> fst
|> should haveLength 1 (* This includes the effect we ticked. *) )

[<Test>]
let ``tick of song finished should apply JetLagged moodlet if the cities are more than 4 timezones apart``
()
=
[ London, NewYork; London, Sydney; NewYork, London; Sydney, London ]
|> List.iter (fun (prevCity, currCity) ->
let moodletEffect =
Simulation.tickOne dummyState (worldMoveEffect prevCity currCity)
|> fst
|> List.item 1 (* Position 0 is the effect we've ticked. *)

match moodletEffect with
| CharacterMoodletsChanged(_, Diff(prevMoodlet, currMoodlet)) ->
prevMoodlet |> should haveCount 0
currMoodlet |> should haveCount 1

let moodlet = currMoodlet |> Set.toList |> List.head

moodlet.MoodletType
|> should be (ofCase <@ MoodletType.JetLagged @>)

moodlet.StartedOn |> should equal dummyToday
| _ -> failwith "Unexpected effect")
Loading

0 comments on commit e35137e

Please sign in to comment.