├── .editorconfig
├── .github
├── copilot-instructions.md
└── workflows
│ └── buildandtest.yml
├── .gitignore
├── .gitmodules
├── Directory.Build.props
├── Dockerfile
├── Duets.sln
├── LICENSE
├── README.md
├── src
├── Duets.Agents
│ ├── Duets.Agents.fsproj
│ ├── LanguageModel.fs
│ ├── Log.fs
│ ├── Savegame.fs
│ ├── State.fs
│ └── Stats.fs
├── Duets.CityExplorer
│ ├── App.axaml
│ ├── App.axaml.fs
│ ├── Duets.CityExplorer.fsproj
│ ├── MainWindow.axaml
│ ├── MainWindow.axaml.fs
│ ├── Program.fs
│ └── app.manifest
├── Duets.Cli
│ ├── Components
│ │ ├── BarChart.fs
│ │ ├── Calendar.fs
│ │ ├── ChoicePrompt.fs
│ │ ├── CityPrompt.fs
│ │ ├── CommandPrompt.fs
│ │ ├── Commands
│ │ │ ├── Aiport
│ │ │ │ ├── BoardPlane.Command.fs
│ │ │ │ ├── PassSecurity.Command.fs
│ │ │ │ └── WaitForLanding.Command.fs
│ │ │ ├── Career
│ │ │ │ └── Work.Command.fs
│ │ │ ├── Cheats
│ │ │ │ ├── Band.Cheats.Commands.fs
│ │ │ │ ├── Cheats.Commands.fs
│ │ │ │ ├── Life.Cheats.Commands.fs
│ │ │ │ ├── Money.Cheats.Commands.fs
│ │ │ │ ├── Skills.Cheats.Commands.fs
│ │ │ │ └── World.Cheats.Commands.fs
│ │ │ ├── Clock.Command.fs
│ │ │ ├── Command.fs
│ │ │ ├── Concert
│ │ │ │ ├── BassSolo.Command.fs
│ │ │ │ ├── ConcertCommon.fs
│ │ │ │ ├── DoEncore.Command.fs
│ │ │ │ ├── DrumSolo.Command.fs
│ │ │ │ ├── FinishConcert.Command.fs
│ │ │ │ ├── GetOffStage.Command.fs
│ │ │ │ ├── GiveSpeech.Command.fs
│ │ │ │ ├── GreetAudience.Command.fs
│ │ │ │ ├── GuitarSolo.Command.fs
│ │ │ │ ├── MakeCrowdSing.Command.fs
│ │ │ │ ├── PerformSoundcheck.Command.fs
│ │ │ │ ├── PlaySong.Commands.fs
│ │ │ │ ├── SetupMerchStand.Command.fs
│ │ │ │ ├── SpinDrumsticks.Command.fs
│ │ │ │ ├── StartConcert.Command.fs
│ │ │ │ └── TuneInstrument.Command.fs
│ │ │ ├── Exit.Command.fs
│ │ │ ├── Gym
│ │ │ │ └── AskForEntrance.Command.fs
│ │ │ ├── Help.Command.fs
│ │ │ ├── Inventory.Command.fs
│ │ │ ├── Items
│ │ │ │ ├── Consume.Command.fs
│ │ │ │ ├── Cook.Command.fs
│ │ │ │ ├── Interactive.Command.fs
│ │ │ │ ├── Open.Command.fs
│ │ │ │ ├── Put.Command.fs
│ │ │ │ └── Sleep.Command.fs
│ │ │ ├── Look.Command.fs
│ │ │ ├── Map.Command.fs
│ │ │ ├── Me.Command.fs
│ │ │ ├── MerchandiseWorkshop
│ │ │ │ ├── ListOrders.Command.fs
│ │ │ │ ├── OrderMerchandise.Command.fs
│ │ │ │ └── PickUpOrder.Command.fs
│ │ │ ├── MiniGame
│ │ │ │ ├── Bet.Command.fs
│ │ │ │ ├── Hit.Command.fs
│ │ │ │ ├── Leave.Command.fs
│ │ │ │ ├── Stand.Command.fs
│ │ │ │ └── StartMiniGame.Command.fs
│ │ │ ├── Movement.Command.fs
│ │ │ ├── Phone.Command.fs
│ │ │ ├── RehearsalRoom
│ │ │ │ ├── BandInventory.Command.fs
│ │ │ │ ├── ComposeSong.Command.fs
│ │ │ │ ├── DiscardSong.Command.fs
│ │ │ │ ├── FinishSong.Command.fs
│ │ │ │ ├── FireMember.Command.fs
│ │ │ │ ├── HireMember.Command.fs
│ │ │ │ ├── ImproveSong.Command.fs
│ │ │ │ ├── ListMembers.Command.fs
│ │ │ │ ├── ListSongs.Command.fs
│ │ │ │ ├── PracticeSong.Command.fs
│ │ │ │ └── SwitchGenre.Command.fs
│ │ │ ├── Shop
│ │ │ │ ├── Buy.Command.fs
│ │ │ │ ├── BuyCar.Command.fs
│ │ │ │ ├── Order.Command.fs
│ │ │ │ └── SeeMenu.Command.fs
│ │ │ ├── Social
│ │ │ │ ├── Social.Command.fs
│ │ │ │ ├── Social.Commands.fs
│ │ │ │ └── StartStopConversation.Command.fs
│ │ │ ├── Studio
│ │ │ │ ├── Common.fs
│ │ │ │ ├── CreateAlbum.Command.fs
│ │ │ │ ├── EditAlbumName.Command.fs
│ │ │ │ ├── ListUnreleasedAlbums.Command.fs
│ │ │ │ ├── RecordSong.Command.fs
│ │ │ │ └── ReleaseAlbum.Command.fs
│ │ │ ├── Travel
│ │ │ │ ├── Drive.Command.fs
│ │ │ │ ├── LeaveVehicle.Command.fs
│ │ │ │ ├── TravelByMetro.Command.fs
│ │ │ │ └── WaitForMetro.Command.fs
│ │ │ └── Wait.Command.fs
│ │ ├── ConcertDetails.fs
│ │ ├── Effect.fs
│ │ ├── Figlet.fs
│ │ ├── GameInfo.fs
│ │ ├── Layout.fs
│ │ ├── Map.fs
│ │ ├── Message.fs
│ │ ├── Notification.fs
│ │ ├── Post.fs
│ │ ├── ProgressBar.fs
│ │ ├── Prompt.fs
│ │ ├── Review.fs
│ │ ├── Separator.fs
│ │ ├── Table.fs
│ │ ├── Tip.fs
│ │ └── VisualEffects.fs
│ ├── Duets.Cli.fsproj
│ ├── Duets.Cli.fsproj.user
│ ├── Program.fs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Scenes.fs
│ ├── Scenes
│ │ ├── Cheats.fs
│ │ ├── MainMenu.fs
│ │ ├── NewGame
│ │ │ ├── BandCreator.fs
│ │ │ ├── CharacterCreator.fs
│ │ │ ├── SkillEditor.fs
│ │ │ └── WorldSelector.fs
│ │ ├── Phone
│ │ │ ├── Apps
│ │ │ │ ├── Bank
│ │ │ │ │ ├── Bank.fs
│ │ │ │ │ ├── DistributeBandFunds.fs
│ │ │ │ │ ├── Transfer.fs
│ │ │ │ │ └── UpcomingPayments.fs
│ │ │ │ ├── BnB
│ │ │ │ │ ├── BnB.fs
│ │ │ │ │ ├── List.fs
│ │ │ │ │ └── Rent.fs
│ │ │ │ ├── Calendar
│ │ │ │ │ └── Calendar.fs
│ │ │ │ ├── ConcertAssistant
│ │ │ │ │ ├── ConcertAssistant.fs
│ │ │ │ │ ├── ScheduleOpeningActShow.fs
│ │ │ │ │ └── ScheduleSoloShow.fs
│ │ │ │ ├── Duber
│ │ │ │ │ └── Duber.fs
│ │ │ │ ├── Flights
│ │ │ │ │ ├── BookFlight.fs
│ │ │ │ │ └── Flights.fs
│ │ │ │ ├── FoodDelivery
│ │ │ │ │ └── FoodDelivery.fs
│ │ │ │ ├── Jobs
│ │ │ │ │ ├── FindJob.fs
│ │ │ │ │ └── Jobs.fs
│ │ │ │ ├── Mastodon
│ │ │ │ │ ├── Commands
│ │ │ │ │ │ ├── Exit.Mastodon.Command.fs
│ │ │ │ │ │ ├── Post.Mastodon.Command.fs
│ │ │ │ │ │ ├── SignUp.Mastodon.Command.fs
│ │ │ │ │ │ ├── SwitchAccount.Mastodon.Command.fs
│ │ │ │ │ │ └── Timeline.Mastodon.Command.fs
│ │ │ │ │ ├── Mastodon.fs
│ │ │ │ │ └── SignUp.fs
│ │ │ │ ├── Statistics
│ │ │ │ │ ├── AlbumsStatistics.fs
│ │ │ │ │ ├── BandStatistics.fs
│ │ │ │ │ ├── RelationshipsStatistics.fs
│ │ │ │ │ ├── ReviewStatistics.fs
│ │ │ │ │ └── Statistics.fs
│ │ │ │ └── Weather
│ │ │ │ │ └── Weather.fs
│ │ │ └── Phone.fs
│ │ ├── Settings.fs
│ │ └── World.fs
│ └── Text
│ │ ├── Career.fs
│ │ ├── Character.fs
│ │ ├── Command.fs
│ │ ├── Concert.fs
│ │ ├── Creator.fs
│ │ ├── Date.fs
│ │ ├── Emoji.fs
│ │ ├── Events.fs
│ │ ├── Generic.fs
│ │ ├── Interaction.fs
│ │ ├── Items.fs
│ │ ├── MainMenu.fs
│ │ ├── MiniGame.fs
│ │ ├── Phone.fs
│ │ ├── Prompts
│ │ ├── CarDealerPrompts.fs
│ │ ├── CommonPrompts.fs
│ │ ├── DrivingPrompts.fs
│ │ ├── DuberPrompts.fs
│ │ ├── FlightPrompts.fs
│ │ ├── WorkPrompts.fs
│ │ └── WorldPrompts.fs
│ │ ├── Rehearsal.fs
│ │ ├── Shop.fs
│ │ ├── Skill.fs
│ │ ├── Social.fs
│ │ ├── Songs.fs
│ │ ├── Studio.fs
│ │ ├── Styles.fs
│ │ ├── Travel.fs
│ │ └── World.fs
├── Duets.Common
│ ├── Duets.Common.fsproj
│ ├── Files.fs
│ ├── Func.fs
│ ├── List.fs
│ ├── Map.fs
│ ├── Math.fs
│ ├── Operators.fs
│ ├── Option.fs
│ ├── Pipe.fs
│ ├── README.md
│ ├── Random.fs
│ ├── Result.fs
│ ├── Seq.fs
│ ├── Serializer.fs
│ ├── String.fs
│ ├── Tuple.fs
│ └── Union.fs
├── Duets.Data
│ ├── ATTRIBUTIONS.md
│ ├── Careers.fs
│ ├── Duets.Data.fsproj
│ ├── Genres.fs
│ ├── Items
│ │ ├── Book.Items.fs
│ │ ├── Drink.Items.fs
│ │ ├── Electronics.Items.fs
│ │ ├── Food
│ │ │ ├── All.Food.fs
│ │ │ ├── Breakfast.Food.fs
│ │ │ ├── Czech.Food.fs
│ │ │ ├── French.Food.fs
│ │ │ ├── Italian.Food.fs
│ │ │ ├── Japanese.Food.fs
│ │ │ ├── Mexican.Food.fs
│ │ │ ├── Snack.Food.fs
│ │ │ ├── Spanish.Food.fs
│ │ │ ├── Turkish.Food.fs
│ │ │ ├── USA.Food.fs
│ │ │ └── Vietnamese.Food.fs
│ │ ├── Furniture.Items.fs
│ │ ├── Gym.Items.fs
│ │ └── Vehicle.Items.fs
│ ├── Migrations.fs
│ ├── Npcs.fs
│ ├── ResourceLoader.fs
│ ├── Resources
│ │ ├── adjectives.json
│ │ ├── adverbs.json
│ │ ├── books.json
│ │ ├── genres.json
│ │ ├── nouns.json
│ │ └── npcs.json
│ ├── Roles.fs
│ ├── Savegame
│ │ ├── Migrations
│ │ │ └── 0_MigrateFromVersionless.fs
│ │ └── Types.fs
│ ├── Skills.fs
│ ├── Util
│ │ └── NpcGen.fsx
│ ├── VocalStyles.fs
│ ├── Words.fs
│ └── World
│ │ ├── Cities
│ │ ├── Layouts.fs
│ │ ├── London
│ │ │ ├── Camden.London.fs
│ │ │ ├── CityOfLondon.London.fs
│ │ │ ├── Greenwich.London.fs
│ │ │ ├── Heathrow.London.fs
│ │ │ ├── London.fs
│ │ │ └── WestEnd.London.fs
│ │ ├── LosAngeles
│ │ │ ├── DowntownLA.LosAngeles.fs
│ │ │ ├── Hollywood.LosAngeles.fs
│ │ │ ├── Ids.LosAngeles.fs
│ │ │ ├── Koreatown.LosAngeles.fs
│ │ │ ├── Lax.LosAngeles.fs
│ │ │ ├── LosAngeles.fs
│ │ │ └── SantaMonica.LosAngeles.fs
│ │ ├── Madrid
│ │ │ ├── Barajas.Madrid.fs
│ │ │ ├── Centro.Madrid.fs
│ │ │ ├── Chamartin.Madrid.fs
│ │ │ ├── Chamberi.Madrid.fs
│ │ │ ├── Madrid.fs
│ │ │ ├── Retiro.Madrid.fs
│ │ │ └── Salamanca.Madrid.fs
│ │ ├── NewYork
│ │ │ ├── Brooklyn.NewYork.fs
│ │ │ ├── Ids.NewYork.fs
│ │ │ ├── Jamaica.NewYork.fs
│ │ │ ├── LowerManhattan.NewYork.fs
│ │ │ ├── MidtownWest.NewYork.fs
│ │ │ └── NewYork.fs
│ │ ├── OpeningHours.fs
│ │ ├── PlaceCreators.fs
│ │ └── Prague
│ │ │ ├── Holešovice.Prague.fs
│ │ │ ├── Ids.Prague.fs
│ │ │ ├── Libeň.Prague.fs
│ │ │ ├── NovéMěsto.Prague.fs
│ │ │ ├── Prague.fs
│ │ │ ├── Ruzyně.Prague.fs
│ │ │ ├── Smíchov.Prague.fs
│ │ │ ├── StaréMěsto.Prague.fs
│ │ │ ├── Vinohrady.Prague.fs
│ │ │ └── Vršovice.Prague.fs
│ │ ├── Ids.fs
│ │ └── World.fs
├── Duets.Entities
│ ├── Album.fs
│ ├── Amount.fs
│ ├── Band.fs
│ ├── BankAccount.fs
│ ├── Calendar.fs
│ ├── CalendarEvent.fs
│ ├── Career.fs
│ ├── Character.fs
│ ├── Concert.fs
│ ├── Duets.Entities.fsproj
│ ├── Flight.fs
│ ├── Identity.fs
│ ├── Instrument.fs
│ ├── Interaction.fs
│ ├── Inventory.fs
│ ├── Item.fs
│ ├── Lenses.fs
│ ├── MiniGame.fs
│ ├── Moodlet.fs
│ ├── README.md
│ ├── Relationships.fs
│ ├── Rental.fs
│ ├── Skill.fs
│ ├── Social.fs
│ ├── SocialNetwork.fs
│ ├── Song.fs
│ ├── State.fs
│ ├── Time.fs
│ ├── Types
│ │ ├── Album.Types.fs
│ │ ├── Attribute.Types.fs
│ │ ├── Band.Types.fs
│ │ ├── Bank.Types.fs
│ │ ├── Book.Types.fs
│ │ ├── Calendar.Types.fs
│ │ ├── CalendarEvent.Types.fs
│ │ ├── Car.Types.fs
│ │ ├── Career.Types.fs
│ │ ├── Character.Types.fs
│ │ ├── City.Types.fs
│ │ ├── Common.Types.fs
│ │ ├── Concert.Types.fs
│ │ ├── Effect.Types.fs
│ │ ├── Flight.Types.fs
│ │ ├── Genre.Types.fs
│ │ ├── Instrument.Types.fs
│ │ ├── Interaction.Types.fs
│ │ ├── Item.Types.fs
│ │ ├── Merch.Types.fs
│ │ ├── MiniGames
│ │ │ ├── Blackjack.Types.fs
│ │ │ ├── MiniGame.Shared.Types.fs
│ │ │ └── MiniGame.Types.fs
│ │ ├── Moodlet.Types.fs
│ │ ├── Notification.Types.fs
│ │ ├── Places
│ │ │ ├── ConcertSpace.Types.fs
│ │ │ ├── Hotel.Types.fs
│ │ │ ├── Metro.Types.fs
│ │ │ ├── RadioStudio.Types.fs
│ │ │ ├── RehearsalSpace.Types.fs
│ │ │ ├── Shop.Types.fs
│ │ │ └── Studio.Types.fs
│ │ ├── Relationship.Types.fs
│ │ ├── Rental.Types.fs
│ │ ├── Situations.Types.fs
│ │ ├── Skill.Types.fs
│ │ ├── Social.Types.fs
│ │ ├── SocialNetwork.Types.fs
│ │ ├── Song.Types.fs
│ │ ├── State.Types.fs
│ │ ├── Weather.Types.fs
│ │ ├── World.Coordinates.Types.fs
│ │ └── World.Types.fs
│ └── World.fs
└── Duets.Simulation
│ ├── Albums
│ ├── DailyStreams.fs
│ ├── DailyUpdate.fs
│ ├── FanIncrease.fs
│ ├── Hype.fs
│ ├── Revenue.fs
│ └── ReviewGeneration.fs
│ ├── Bands
│ ├── FundDistribution.fs
│ ├── Generation.fs
│ ├── Members.fs
│ └── SwitchGenre.fs
│ ├── Bank
│ └── Operations.fs
│ ├── Careers
│ ├── Common.fs
│ ├── Employment.fs
│ ├── JobBoard.fs
│ ├── Promotion.fs
│ ├── RequirementCharacterUpgrade.fs
│ └── Work.fs
│ ├── Character
│ ├── AttributeChange.fs
│ ├── Attributes.fs
│ ├── Moodlets.fs
│ └── Npc.fs
│ ├── Concerts
│ ├── DailyUpdate.fs
│ ├── Live
│ │ ├── Live.Actions.fs
│ │ ├── Live.Common.fs
│ │ ├── Live.Encore.fs
│ │ └── Live.Finish.fs
│ ├── OpeningActOpportunities.fs
│ ├── Preparation
│ │ └── StartConcertPreparation.fs
│ └── Scheduler.fs
│ ├── Config.fs
│ ├── Cooking
│ └── Cooking.fs
│ ├── Duets.Simulation.fsproj
│ ├── EffectModifiers
│ ├── EffectModifiers.fs
│ └── Moodlets.EffectModifiers.fs
│ ├── Events
│ ├── Band
│ │ ├── Band.Events.fs
│ │ ├── Relationships.fs
│ │ └── Reviews.fs
│ ├── Career.Events.fs
│ ├── Character
│ │ ├── Character.Events.fs
│ │ ├── Drunkenness.fs
│ │ ├── Fame.fs
│ │ ├── Hospitalization.fs
│ │ └── Hunger.fs
│ ├── Concert.Events.fs
│ ├── Events.fs
│ ├── Moodlets
│ │ ├── Cleanup.fs
│ │ ├── JetLagged.fs
│ │ ├── Moodlets.Events.fs
│ │ ├── NotInspired.fs
│ │ └── TiredOfTouring.fs
│ ├── NonInteractiveGame.Events.fs
│ ├── Place
│ │ ├── ClosingTime.fs
│ │ └── RentalExpiration.fs
│ ├── Skill.Events.fs
│ ├── Time.Events.fs
│ ├── Types.fs
│ └── World.Events.fs
│ ├── Flights
│ ├── Airport.fs
│ ├── Booking.fs
│ └── TicketGeneration.fs
│ ├── Gym
│ └── PayEntrance.fs
│ ├── Interactions
│ ├── Items
│ │ ├── Actions
│ │ │ ├── Exercise.Action.fs
│ │ │ └── Read.Action.fs
│ │ ├── Drink.Interactions.fs
│ │ ├── Food.Interactions.fs
│ │ └── Item.Interactions.fs
│ └── Sleep.fs
│ ├── Items
│ └── Items.fs
│ ├── Market
│ └── GenreMarket.fs
│ ├── Merchandise
│ ├── Order.Merchandise.fs
│ ├── PickUp.Merchandise.fs
│ ├── Sell.Merchandise.fs
│ └── SetPrice.Merchandise.fs
│ ├── MiniGames
│ └── Blackjack.fs
│ ├── Notifications
│ └── Notifications.fs
│ ├── Queries
│ ├── Albums.fs
│ ├── Bands.fs
│ ├── Bank.fs
│ ├── Calendar.fs
│ ├── CalendarEvents.fs
│ ├── Career.fs
│ ├── Characters.fs
│ ├── Concerts.fs
│ ├── Flights.fs
│ ├── Genres.fs
│ ├── Gym.fs
│ ├── Interactions
│ │ ├── InteractionCommon.fs
│ │ ├── Interactions.Airport.fs
│ │ ├── Interactions.Career.fs
│ │ ├── Interactions.Casino.fs
│ │ ├── Interactions.ConcertSpace.fs
│ │ ├── Interactions.FreeRoam.fs
│ │ ├── Interactions.Gym.fs
│ │ ├── Interactions.Items.fs
│ │ ├── Interactions.MerchandiseWorkshop.fs
│ │ ├── Interactions.MetroStation.fs
│ │ ├── Interactions.RehearsalSpace.fs
│ │ ├── Interactions.Shop.fs
│ │ ├── Interactions.Social.fs
│ │ ├── Interactions.Studio.fs
│ │ ├── Interactions.fs
│ │ └── Requirements
│ │ │ ├── Interactions.Requirements.Energy.fs
│ │ │ └── Interactions.Requirements.Health.fs
│ ├── Inventory.fs
│ ├── Items.fs
│ ├── Merch.fs
│ ├── Metro.fs
│ ├── Notifications.fs
│ ├── Relationships.fs
│ ├── Rentals.fs
│ ├── Shop.fs
│ ├── Situations.fs
│ ├── Skills.fs
│ ├── SocialNetworks.fs
│ ├── Songs.fs
│ └── World.fs
│ ├── RandomGen.fs
│ ├── Rentals
│ ├── PayUpcoming.fs
│ └── RentPlace.fs
│ ├── Setup
│ └── StartGame.fs
│ ├── Shop
│ └── Shop.fs
│ ├── Simulation.fs
│ ├── Situations
│ └── Situations.fs
│ ├── Skills
│ ├── ImproveSkills.Common.fs
│ └── ImproveSkills.Composition.fs
│ ├── Social
│ ├── LongTimeNoSee.fs
│ ├── Relationship.fs
│ ├── Social.Actions.fs
│ └── Social.Common.fs
│ ├── SocialNetworks
│ ├── DailyUpdate.fs
│ ├── Reposts.fs
│ └── SocialNetworks.fs
│ ├── Songs
│ ├── Composition
│ │ ├── Common.fs
│ │ ├── ComposeSong.fs
│ │ ├── DiscardSong.fs
│ │ ├── FinishSong.fs
│ │ └── ImproveSong.fs
│ └── Practice.fs
│ ├── State
│ ├── Albums.fs
│ ├── Bands.fs
│ ├── Bank.fs
│ ├── Calendar.fs
│ ├── Career.fs
│ ├── Characters.fs
│ ├── Concerts.fs
│ ├── Flights.fs
│ ├── Inventory.fs
│ ├── Market.fs
│ ├── Merch.fs
│ ├── Notifications.fs
│ ├── Relationships.fs
│ ├── Rentals.fs
│ ├── Skills.fs
│ ├── SocialNetworks.fs
│ ├── Songs.fs
│ ├── State.fs
│ └── World.fs
│ ├── Studio
│ ├── RecordAlbum.fs
│ ├── ReleaseAlbum.fs
│ └── RenameAlbum.fs
│ ├── Time
│ ├── AdvanceTime.fs
│ └── InteractionMinutes.fs
│ ├── Travel
│ └── Metro.fs
│ ├── Vehicles
│ ├── Car.fs
│ └── Taxi.fs
│ ├── Weather
│ └── WeatherTransition.fs
│ ├── World
│ └── Population.fs
│ └── WorldNavigation
│ ├── Navigation.fs
│ ├── Pathfinding.fs
│ ├── Policies
│ ├── Concert.Policies.fs
│ ├── OpeningHours.Policies.fs
│ ├── Rental.Policies.fs
│ └── Room.Policies.fs
│ └── TravelTime.fs
└── tests
├── Agents.Tests
├── Agents.Tests.fsproj
└── Program.fs
├── Data.Tests
├── Data.Tests.fsproj
├── Items.Tests.fs
├── Program.fs
├── SavegameMigrations.Tests.fs
└── World.Tests.fs
├── Entities.Tests
├── Album.Tests.fs
├── Calendar.Tests.fs
├── Concert.Tests.fs
├── Entities.Tests.fsproj
├── Program.fs
├── SocialNetwork.Tests.fs
├── Time.Tests.fs
└── World.Tests.fs
├── Simulation.Tests
├── Airport
│ └── BoardPlane.Tests.fs
├── Albums
│ ├── DailyUpdate.Tests.fs
│ └── ReviewGeneration.Tests.fs
├── Bands
│ ├── BandGeneration.Tests.fs
│ ├── DistributeFunds.Tests.fs
│ ├── FireMember.Tests.fs
│ └── HireMember.Tests.fs
├── Bank
│ ├── Queries.Tests.fs
│ └── Transfer.Tests.fs
├── Careers
│ ├── Employment.Test.fs
│ ├── JobBoard.Test.fs
│ └── Work.Test.fs
├── Concerts
│ ├── DailyUpdate.Tests.fs
│ ├── Live.DedicateSong.Tests.fs
│ ├── Live.Encore.Tests.fs
│ ├── Live.Finish.Tests.fs
│ ├── Live.GreetAudience.Tests.fs
│ ├── Live.PlaySong.Tests.fs
│ └── OpeningActOpportunities.Tests.fs
├── EffectModifiers
│ └── Moodlet.EffectModifiers.Tests.fs
├── Events
│ ├── Band.Events.Tests.fs
│ ├── Career.Events.Tests.fs
│ ├── Character.Events.Tests.fs
│ ├── ClosingTime.Events.Tests.fs
│ ├── Moodlets
│ │ ├── JetLagged.Moodlet.Events.Tests.fs
│ │ └── NotInspired.Moodlet.Events.Tests.fs
│ ├── Rental.Events.Tests.fs
│ └── World.Events.Tests.fs
├── Interactions
│ ├── Drink.Interactions.Test.fs
│ ├── Exercise.Interactions.Test.fs
│ ├── Food.Interactions.Test.fs
│ ├── Play.Interactions.Test.fs
│ ├── Read.Test.fs
│ └── Sleep.Test.fs
├── Items
│ ├── Items.Put.Tests.fs
│ ├── Items.Remove.Tests.fs
│ └── Items.Take.Tests.fs
├── Market
│ └── GenreMarket.Tests.fs
├── Merchandise
│ ├── Order.Merchandise.Tests.fs
│ ├── PickUp.Merchandise.Tests.fs
│ └── Sell.Merchandise.Tests.fs
├── MiniGames
│ └── Blackjack.Tests.fs
├── Notifications
│ └── Notification.Tests.fs
├── Program.fs
├── Simulation.Tests.fs
├── Simulation.Tests.fsproj
├── Skills
│ └── ImproveSkills.Tests.fs
├── Social
│ └── LongTimeNoSee.Tests.fs
├── SocialNetworks
│ ├── DailyUpdate.Tests.fs
│ └── Reposts.Tests.fs
├── Songs
│ ├── ComposeSong.Tests.fs
│ ├── DiscardSong.Tests.fs
│ ├── FinishSong.Tests.fs
│ ├── ImproveSong.Tests.fs
│ └── PracticeSong.Tests.fs
├── State
│ ├── State.Albums.Tests.fs
│ ├── State.BandManagement.Tests.fs
│ ├── State.Bank.Tests.fs
│ ├── State.Concerts.Tests.fs
│ ├── State.Inventory.Tests.fs
│ ├── State.Merch.Tests.fs
│ ├── State.Relationships.Tests.fs
│ ├── State.Setup.Tests.fs
│ ├── State.Skills.Tests.fs
│ └── State.SongComposition.Tests.fs
├── Studio
│ ├── RecordAlbum.Tests.fs
│ └── RenameAlbum.Tests.fs
├── Time
│ └── AdvanceTime.Tests.fs
├── Vehicles
│ ├── Car.Tests.fs
│ └── Taxi.Tests.fs
├── Weather
│ └── WeatherTransition.Tests.fs
└── World
│ ├── Pathfinding.Tests.fs
│ ├── Population.Tests.fs
│ └── Traveling.Tests.fs
└── Test.Common
├── Generators
├── Character.Generator.fs
├── Concert.Generator.fs
├── Date.Generator.fs
├── Song.Generator.fs
└── State.Generator.fs
├── Library.fs
└── Test.Common.fsproj
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.fs]
4 | indent_style = space
5 | indent_size = 4
6 | max_line_length = 80
7 |
--------------------------------------------------------------------------------
/.github/workflows/buildandtest.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | submodules: 'recursive'
18 | - name: Setup dotnet
19 | uses: actions/setup-dotnet@v3
20 | with:
21 | dotnet-version: '9.0'
22 | include-prerelease: true
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "deps/spectre.console"]
2 | path = deps/spectre.console
3 | url = https://github.com/sleepyfran/spectre.console.git
4 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | net9.0
4 |
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:9.0
2 |
3 | COPY *.sln .
4 | COPY deps ./deps
5 | COPY src ./src
6 | COPY tests ./tests
7 |
8 | RUN dotnet build
9 | ENTRYPOINT ["dotnet", "run", "--project", "src/Cli/Cli.fsproj"]
10 |
--------------------------------------------------------------------------------
/src/Duets.CityExplorer/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Duets.CityExplorer/App.axaml.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.CityExplorer
2 |
3 | open Avalonia
4 | open Avalonia.Controls.ApplicationLifetimes
5 | open Avalonia.Markup.Xaml
6 |
7 | type App() =
8 | inherit Application()
9 |
10 | override this.Initialize() =
11 | AvaloniaXamlLoader.Load(this)
12 |
13 | override this.OnFrameworkInitializationCompleted() =
14 | match this.ApplicationLifetime with
15 | | :? IClassicDesktopStyleApplicationLifetime as desktop ->
16 | desktop.MainWindow <- MainWindow()
17 | | _ -> ()
18 |
19 | base.OnFrameworkInitializationCompleted()
--------------------------------------------------------------------------------
/src/Duets.CityExplorer/Program.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.CityExplorer
2 |
3 | open System
4 | open Avalonia
5 |
6 | module Program =
7 |
8 | []
9 | let buildAvaloniaApp () =
10 | AppBuilder
11 | .Configure()
12 | .UsePlatformDetect()
13 | .WithInterFont()
14 | .LogToTrace(areas = Array.empty)
15 |
16 | []
17 | let main argv =
18 | buildAvaloniaApp().StartWithClassicDesktopLifetime(argv)
--------------------------------------------------------------------------------
/src/Duets.CityExplorer/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/BarChart.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.BarChart
3 |
4 | open Spectre.Console
5 |
6 | /// Returns the associated color given the level of a skill or the quality
7 | /// of a song.
8 | let private colorForLevel level =
9 | match level with
10 | | level when level < 30 -> Color.Red
11 | | level when level < 60 -> Color.Orange1
12 | | level when level < 80 -> Color.Green
13 | | _ -> Color.Blue
14 |
15 | ///
16 | /// Shows a bar chart with a max value of 100.
17 | ///
18 | /// List of tuples of value and text to display
19 | let showBarChart items =
20 | let mutable barChart = BarChart()
21 | barChart.MaxValue <- 100.0
22 |
23 | barChart <-
24 | barChart.AddItems(
25 | items,
26 | fun (progress, label) ->
27 | BarChartItem(label, float progress, colorForLevel progress)
28 | )
29 |
30 | AnsiConsole.Write(barChart)
31 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/CityPrompt.fs:
--------------------------------------------------------------------------------
1 | []
2 | module rec Duets.Cli.Components.CityPrompt
3 |
4 | open Duets.Agents
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 | open Duets.Simulation
8 |
9 | ///
10 | /// Shows a prompt to select a city, with the current city at the top and the
11 | /// rest ordered alphabetically.
12 | ///
13 | /// Prompt to show the user
14 | let showCityPrompt prompt =
15 | let currentCity = Queries.World.currentCity (State.get ())
16 |
17 | showOptionalChoicePrompt
18 | prompt
19 | Generic.cancel
20 | (fun (city: City) ->
21 | if city.Id = currentCity.Id then
22 | $"{Generic.cityName city.Id} (Current)" |> Styles.highlight
23 | else
24 | Generic.cityName city.Id)
25 | (sortedCities currentCity)
26 |
27 | /// Lists all available cities with the current one at the top.
28 | let private sortedCities currentCity =
29 | let allButCurrentCity =
30 | Queries.World.allCities
31 | |> List.filter (fun city -> city.Id <> currentCity.Id)
32 | |> List.sortBy (fun city -> Generic.cityName city.Id)
33 |
34 | currentCity :: allButCurrentCity
35 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Aiport/BoardPlane.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli
4 | open Duets.Cli.Components
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
8 | open Duets.Simulation.Flights.Airport
9 |
10 | []
11 | module BoardPlaneCommand =
12 | /// Command that allows the user to board the plane and start their trip.
13 | let create flight =
14 | { Name = "board plane"
15 | Description = Command.boardPlaneDescription flight
16 | Handler =
17 | fun _ ->
18 | showProgressBarAsync [ Travel.waitingToBoard ] 5
19 |
20 | let effects, flightTime = boardPlane flight
21 |
22 | Travel.planeBoarded flight flightTime |> showMessage
23 |
24 | effects |> Effect.applyMultiple
25 |
26 | Scene.World }
27 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Aiport/PassSecurity.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.SceneIndex
7 | open Duets.Cli.Text
8 | open Duets.Entities
9 | open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
10 | open Duets.Simulation.Flights.Airport
11 |
12 | []
13 | module PassSecurityCommand =
14 | /// Command that allows the user to pass the security check in the airport.
15 | let get =
16 | { Name = "pass security check"
17 | Description = Command.passSecurityCheckDescription
18 | Handler =
19 | fun _ ->
20 | showProgressBarSync [ Travel.passingSecurityCheck ] 5
21 |
22 | let effects = passSecurityCheck (State.get ())
23 | Effect.applyMultiple effects
24 |
25 | let takenItemsEffect =
26 | effects
27 | |> List.filter (function
28 | | ItemRemovedFromCharacterInventory _ -> true
29 | | _ -> false)
30 |
31 | match takenItemsEffect with
32 | | eff when eff.Length > 0 ->
33 | Travel.itemsTakenBySecurity |> showMessage
34 | | _ -> ()
35 |
36 | Scene.WorldAfterMovement }
37 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Aiport/WaitForLanding.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.SceneIndex
7 | open Duets.Cli.Text
8 | open Duets.Cli.Text.Prompts
9 | open Duets.Simulation.Flights.Airport
10 |
11 | []
12 | module WaitForLandingCommand =
13 | /// Command that allows the user to wait until the flight finishes.
14 | let create flight =
15 | { Name = "wait"
16 | Description = Command.waitForLandingDescription
17 | Handler =
18 | fun _ ->
19 | let state = State.get ()
20 |
21 | Flight.createInFlightExperiencePrompt state flight
22 | |> LanguageModel.streamMessage
23 | |> streamStyled Styles.event
24 |
25 | lineBreak ()
26 | lineBreak ()
27 | wait 2000
28 |
29 | Flight.createAirportExperiencePrompt state flight
30 | |> LanguageModel.streamMessage
31 | |> streamStyled Styles.event
32 |
33 | lineBreak ()
34 | wait 1500
35 |
36 | lineBreak ()
37 |
38 | leavePlane (State.get ()) flight |> Effect.applyMultiple
39 |
40 | Scene.WorldAfterMovement }
41 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Cheats/Cheats.Commands.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands.Cheats
2 |
3 | open Duets.Cli.Components.Commands
4 | open Duets.Cli.SceneIndex
5 |
6 | module Index =
7 | let all =
8 | [ BandCommands.makeMeABand
9 | BandCommands.pactWithTheDevil
10 | LifeCommands.happy
11 | LifeCommands.notMoody
12 | LifeCommands.roaming
13 | LifeCommands.spotlight
14 | LifeCommands.timeTravel
15 | MoneyCommands.moneyHeist
16 | MoneyCommands.motherlode
17 | MoneyCommands.rosebud
18 | SkillCommands.pureSkill
19 | WorldCommands.teleport ]
20 |
21 | let enterCommand =
22 | { Name = "iwanttoskipreality"
23 | Description = ""
24 | Handler = fun _ -> Scene.Cheats }
25 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/BassSolo.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Text
4 | open Duets.Simulation.Concerts.Live
5 |
6 | []
7 | module BassSoloCommand =
8 | /// Command to perform a bass solo.
9 | let create ongoingConcert =
10 | Concert.createSoloCommand
11 | "bass solo"
12 | Command.bassSoloDescription
13 | [ Concert.bassSoloMovingFingersQuickly
14 | Concert.bassSoloSlappingThatBass
15 | Concert.bassSoloGrooving ]
16 | bassSolo
17 | ongoingConcert
18 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/DoEncore.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.Components.Commands
6 | open Duets.Cli.Text
7 | open Duets.Simulation.Concerts.Live.Encore
8 |
9 | []
10 | module DoEncoreCommand =
11 | /// Returns the artist back to the stage to perform an encore. Assumes that
12 | /// an encore is possible and that the audience will still be there for it.
13 | let create ongoingConcert =
14 | Concert.createCommand
15 | "do encore"
16 | Command.doEncoreDescription
17 | (fun _ -> doEncore (State.get ()))
18 | (fun _ _ -> Concert.encoreComingBackToStage |> showMessage)
19 | ongoingConcert
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/DrumSolo.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Text
4 | open Duets.Simulation.Concerts.Live
5 |
6 | []
7 | module DrumSoloCommand =
8 | /// Command to perform a drum solo.
9 | let create ongoingConcert =
10 | Concert.createSoloCommand
11 | "drum solo"
12 | Command.drumSoloDescription
13 | [ Concert.drumSoloDoingDrumstickTricks
14 | Concert.drumSoloPlayingWeirdRhythms
15 | Concert.drumSoloPlayingReallyFast ]
16 | drumSolo
17 | ongoingConcert
18 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/FinishConcert.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Duets.Simulation
8 |
9 | []
10 | module FinishConcertCommand =
11 | /// Puts the artist out of the ongoing concert scene, which shows them the
12 | /// total points accumulated during the concert, the result of it and allows
13 | /// them to move to other places outside the stage/backstage.
14 | let rec create ongoingConcert =
15 | { Name = "finish concert"
16 | Description = Command.finishConcertDescription
17 | Handler =
18 | (fun _ ->
19 | Concerts.Live.Finish.finishConcert
20 | (State.get ())
21 | ongoingConcert
22 | |> Duets.Cli.Effect.applyMultiple
23 |
24 | Scene.World) }
25 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/GetOffStage.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live.Encore
7 |
8 | []
9 | module GetOffStageCommand =
10 | /// Command which moves the person from the stage into the backstage. This
11 | /// might end the concert if people is not really interested in staying for
12 | /// the encore.
13 | let rec create ongoingConcert =
14 | Concert.createCommand
15 | "get off stage"
16 | Command.getOffStageDescription
17 | getOffStage
18 | (fun canPerformEncore _ ->
19 | lineBreak ()
20 |
21 | if canPerformEncore then
22 | Concert.getOffStageEncorePossible |> showMessage
23 |
24 | lineBreak ()
25 | else
26 | Concert.getOffStageNoEncorePossible |> showMessage
27 |
28 | lineBreak ())
29 | ongoingConcert
30 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/GiveSpeech.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live
7 |
8 | []
9 | module GiveSpeechCommand =
10 | /// Command which simulates giving a speech during a concert.
11 | let rec create ongoingConcert =
12 | Concert.createCommand
13 | "give speech"
14 | Command.giveSpeechDescription
15 | giveSpeech
16 | (fun result points ->
17 | Concert.showSpeechProgress ()
18 |
19 | match result with
20 | | LowPerformance _ -> Concert.speechGivenLowSkill points
21 | | AveragePerformance _ -> Concert.speechGivenMediumSkill points
22 | | GoodPerformance _
23 | | GreatPerformance -> Concert.speechGivenHighSkill points
24 | | _ -> Concert.tooManySpeeches
25 | |> showMessage)
26 | ongoingConcert
27 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/GreetAudience.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live
7 |
8 | []
9 | module GreetAudienceCommand =
10 | /// Command which greets the audience in the concert.
11 | let rec create ongoingConcert =
12 | Concert.createCommand
13 | "greet audience"
14 | Command.greetAudienceDescription
15 | greetAudience
16 | (fun result points ->
17 | match result with
18 | | TooManyRepetitionsPenalized
19 | | TooManyRepetitionsNotDone ->
20 | Concert.greetAudienceGreetedMoreThanOnceTip points
21 | | _ -> Concert.greetAudienceDone points
22 | |> showMessage)
23 | ongoingConcert
24 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/GuitarSolo.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Text
4 | open Duets.Simulation.Concerts.Live
5 |
6 | []
7 | module GuitarSoloCommand =
8 | /// Command to perform a guitar solo.
9 | let create ongoingConcert =
10 | Concert.createSoloCommand
11 | "guitar solo"
12 | Command.guitarSoloDescription
13 | [ Concert.guitarSoloPlayingReallyFast
14 | Concert.guitarSoloPlayingWithTeeth
15 | Concert.guitarSoloDoingSomeTapping ]
16 | guitarSolo
17 | ongoingConcert
18 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/MakeCrowdSing.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live
7 |
8 | []
9 | module MakeCrowdSingCommand =
10 | /// Command which allows the user make the crowd sing.
11 | let rec create ongoingConcert =
12 | Concert.createCommand
13 | "make crowd sing"
14 | Command.makeCrowdSingDescription
15 | makeCrowdSing
16 | (fun result points ->
17 | match result with
18 | | LowPerformance _ ->
19 | Concert.makeCrowdSingLowPerformance points
20 | | AveragePerformance _ ->
21 | Concert.makeCrowdSingAveragePerformance points
22 | | GoodPerformance _
23 | | GreatPerformance ->
24 | Concert.makeCrowdSingGreatPerformance points
25 | | _ -> Concert.tooMuchSingAlong
26 | |> showMessage)
27 | ongoingConcert
28 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/PerformSoundcheck.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.Components.Commands
7 | open Duets.Cli.SceneIndex
8 | open Duets.Cli.Text
9 | open Duets.Simulation.Concerts
10 | open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
11 |
12 | []
13 | module PerformSoundcheckCommand =
14 | /// Returns a command that marks the soundcheck as done.
15 | let create checklist =
16 | { Name = "soundcheck"
17 | Description =
18 | "Allows you to check the sound of your band before the concert starts, which improves the quality of the concert"
19 | Handler =
20 | fun _ ->
21 | showProgressBarSync
22 | [ "Plugin cables..." |> Styles.progress
23 | "Mic check, mic check..." |> Styles.progress
24 | "Staring into the abyss while the rest of the band checks..."
25 | |> Styles.progress ]
26 | 1
27 |
28 | Live.Actions.soundcheck (State.get ()) checklist
29 | |> Effect.applyMultiple
30 |
31 | Scene.World }
32 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/SpinDrumsticks.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live
7 |
8 | []
9 | module SpinDrumsticksCommand =
10 | /// Command which performs the action of spinning the drumsticks for drummers.
11 | let rec create ongoingConcert =
12 | Concert.createCommand
13 | "spin drumstick"
14 | Command.makeCrowdSingDescription
15 | spinDrumsticks
16 | (fun result points ->
17 | match result with
18 | | LowPerformance _ -> Concert.drumstickSpinningBadResult points
19 | | AveragePerformance _
20 | | GoodPerformance _
21 | | GreatPerformance ->
22 | Concert.drumstickSpinningGoodResult points
23 | | _ -> Concert.tooManyDrumstickSpins
24 | |> showMessage)
25 | ongoingConcert
26 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Concert/TuneInstrument.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Simulation.Concerts.Live
7 |
8 | []
9 | module TuneInstrumentCommand =
10 | /// Command which allows the player to tune their instrument mid-concert.
11 | let rec create ongoingConcert =
12 | Concert.createCommand
13 | "tune instrument"
14 | Command.tuneInstrumentDescription
15 | tuneInstrument
16 | (fun result points ->
17 | match result with
18 | | Done -> Concert.tuneInstrumentDone points
19 | | _ -> Concert.tooMuchTuning
20 | |> showMessage)
21 | ongoingConcert
22 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Exit.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.SceneIndex
4 | open Duets.Cli.Text
5 |
6 | []
7 | module ExitCommand =
8 | /// Command which exits the app upon being called.
9 | let get =
10 | { Name = "exit"
11 | Description = Command.exitDescription
12 | Handler = fun _ -> Scene.Exit ExitMode.SaveGame }
13 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Gym/AskForEntrance.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.SceneIndex
7 | open Duets.Cli.Text
8 | open Duets.Simulation.Bank.Operations
9 | open Duets.Simulation.Gym
10 |
11 | []
12 | module AskForEntranceCommand =
13 | /// Command to pay for a one-time entrance to a gym.
14 | let create entranceFee =
15 | { Name = "ask for entrance"
16 | Description = "Allows you to pay for a one-time entrance to a gym"
17 | Handler =
18 | (fun _ ->
19 | let confirmed =
20 | $"The entrance fee is {Styles.money entranceFee}. Do you want to pay it?"
21 | |> showConfirmationPrompt
22 |
23 | if confirmed then
24 | let result = Entrance.pay (State.get ()) entranceFee
25 |
26 | match result with
27 | | Ok effects -> effects |> Effect.applyMultiple
28 | | Error(NotEnoughFunds _) ->
29 | Shop.notEnoughFunds |> showMessage
30 |
31 | Scene.World) }
32 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Inventory.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 |
7 | []
8 | module InventoryCommand =
9 | /// Command which displays what the character is currently carrying in their
10 | /// inventory.
11 | let create inventory =
12 | { Name = "inventory"
13 | Description = Command.inventoryDescription
14 | Handler =
15 | fun _ ->
16 | if List.isEmpty inventory then
17 | Items.noItemsInventory |> showMessage
18 | else
19 | Items.itemsCurrentlyCarrying |> showMessage
20 |
21 | inventory |> List.map Items.itemRow |> List.iter showMessage
22 |
23 | Scene.World }
24 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/MerchandiseWorkshop/PickUpOrder.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.SceneIndex
6 | open Duets.Entities
7 | open Duets.Simulation.Merchandise.PickUp
8 |
9 | []
10 | module PickUpMerchandiseOrdersCommand =
11 | /// Command to pick up all the merchandise orders that are available in the shop.
12 | let create (items: Item list) =
13 | { Name = "pick order"
14 | Description =
15 | "Allows you to pick up any order that is already available"
16 | Handler =
17 | fun _ ->
18 | pickUpOrder (State.get ()) items |> Effect.applyMultiple
19 |
20 | Scene.World }
21 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/MiniGame/Leave.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.SceneIndex
7 | open Duets.Cli.Text
8 | open Duets.Simulation.MiniGames
9 |
10 | []
11 | module LeaveCommand =
12 | /// Command which allows the player to leave the current mini-game.
13 | let create miniGameId miniGameState =
14 | { Name = "leave"
15 | Description =
16 | $"Allows you to leave the {Generic.miniGameName miniGameId} game"
17 | Handler =
18 | (fun _ ->
19 | let result = Blackjack.leave miniGameState
20 |
21 | match result with
22 | | Ok effect -> effect |> Effect.applyMultiple
23 | | Error Blackjack.NotAllowed ->
24 | "You can't leave the game now!"
25 | |> Styles.error
26 | |> showMessage
27 |
28 | Scene.World) }
29 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/MiniGame/StartMiniGame.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 | open Duets.Simulation
7 |
8 | []
9 | module StartMiniGameCommand =
10 | /// Command which starts a mini-game given its ID.
11 | let create miniGameId =
12 | { Name = $"play {Generic.miniGameName miniGameId}"
13 | Description =
14 | $"Allows you to start a game of {Generic.miniGameName miniGameId}"
15 | Handler =
16 | (fun _ ->
17 | MiniGames.Blackjack.startGame |> Effect.apply
18 |
19 | Scene.World) }
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Phone.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.SceneIndex
4 | open Duets.Cli.Text
5 |
6 | []
7 | module PhoneCommand =
8 | /// Command which opens the phone of the user.
9 | let get =
10 | { Name = "phone"
11 | Description = Command.phoneDescription
12 | Handler = fun _ -> Scene.Phone }
13 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/RehearsalRoom/BandInventory.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 | open Duets.Common
7 | open Duets.Entities
8 |
9 | []
10 | module BandInventoryCommand =
11 | /// Command that displays the inventory of the band.
12 | let create (items: (Item * int) list) =
13 | { Name = "band inventory"
14 | Description = "Shows all the merchandise your band has in stock"
15 | Handler =
16 | (fun _ ->
17 | "Your band currently has:" |> showMessage
18 |
19 | items
20 | |> List.iter (fun (item, quantity) ->
21 | $"- {quantity} {Generic.simplePluralOf (item.Name |> String.lowercase) quantity |> Styles.item}"
22 | |> showMessage)
23 |
24 | Scene.World) }
25 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Shop/Buy.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Duets.Simulation
8 |
9 | []
10 | module BuyCommand =
11 | /// Command to buy something from a shop by specifying the name of the item
12 | /// via the command arguments or selecting it interactively.
13 | let create availableItems =
14 | { Name = "buy"
15 | Description = Command.buyDescription
16 | Handler =
17 | fun _ ->
18 | let selectedItem =
19 | showSearchableOptionalChoicePrompt
20 | Shop.itemPrompt
21 | Generic.cancel
22 | Shop.itemInteractiveRow
23 | availableItems
24 |
25 | match selectedItem with
26 | | Some item ->
27 | let orderResult = Shop.order (State.get ()) item
28 |
29 | match orderResult with
30 | | Ok effects -> Duets.Cli.Effect.applyMultiple effects
31 | | Error _ -> Shop.notEnoughFunds |> showMessage
32 | | None -> ()
33 |
34 | Scene.World }
35 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Shop/SeeMenu.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 |
8 | []
9 | module SeeMenuCommand =
10 | /// Command to display the menu available on a restaurant or bar.
11 | let create (availableItems: PurchasableItem list) =
12 | { Name = "see menu"
13 | Description = Command.seeMenuDescription
14 | Handler =
15 | (fun _ ->
16 | let tableColumns =
17 | [ Shop.itemNameHeader
18 | Shop.itemTypeHeader
19 | Shop.itemPriceHeader ]
20 |
21 | let tableRows =
22 | availableItems
23 | |> List.sortBy (fun (item, _) -> item.Brand)
24 | |> List.map (fun (item, price) ->
25 | [ item.Brand
26 | Shop.itemType item
27 | Shop.itemPrice price ])
28 |
29 | showTable tableColumns tableRows
30 |
31 | Scene.World) }
32 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Social/Social.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli
4 | open Duets.Cli.SceneIndex
5 | open Duets.Simulation.Social.Common
6 |
7 | []
8 | module SocialCommand =
9 | /// Creates a command that calls the given action and applies all the
10 | /// effects returned by the response, then returns the world scene.
11 | let create
12 | (args:
13 | {| Name: string
14 | Description: string
15 | Action: unit -> SocialActionResponse
16 | Handler: SocialActionResult -> unit |})
17 | =
18 | { Name = args.Name
19 | Description = args.Description
20 | Handler =
21 | fun _ ->
22 | let response = args.Action()
23 | response.Result |> args.Handler
24 | response.Effects |> Effect.applyMultiple
25 |
26 | Scene.World }
27 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Studio/Common.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 | open Duets.Simulation.Studio.ReleaseAlbum
8 |
9 | module Studio =
10 | /// Shows an error indicating what made the album name validation fail.
11 | let showAlbumNameError error =
12 | match error with
13 | | Album.NameTooShort -> Studio.createErrorNameTooShort
14 | | Album.NameTooLong -> Studio.createErrorNameTooLong
15 | |> showMessage
16 |
17 | /// Shows a prompt that asks the user if they want to release an album and
18 | /// handles the release.
19 | let promptToReleaseAlbum band unreleasedAlbum =
20 | let album = unreleasedAlbum |> Album.fromUnreleased
21 |
22 | let state = State.get ()
23 |
24 | let confirmed =
25 | showConfirmationPrompt (
26 | Studio.commonPromptReleaseAlbum album.Name album.Type
27 | )
28 |
29 | if confirmed then
30 | releaseAlbum state band unreleasedAlbum |> Duets.Cli.Effect.apply
31 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Studio/ReleaseAlbum.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Duets.Entities
8 | open Duets.Simulation
9 |
10 | []
11 | module ReleaseAlbumCommand =
12 | /// Command to release an unreleased album.
13 | let create unreleasedAlbums =
14 | { Name = "release album"
15 | Description = Command.releaseAlbumDescription
16 | Handler =
17 | (fun _ ->
18 | let state = State.get ()
19 |
20 | let currentBand = Queries.Bands.currentBand state
21 |
22 | showOptionalChoicePrompt
23 | "Which album do you want to release?"
24 | Generic.cancel
25 | (fun (unreleasedAlbum: UnreleasedAlbum) ->
26 | unreleasedAlbum.Album.Name)
27 | unreleasedAlbums
28 | |> Option.iter (Studio.promptToReleaseAlbum currentBand)
29 |
30 | Scene.World) }
31 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Travel/LeaveVehicle.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli
4 | open Duets.Cli.SceneIndex
5 | open Duets.Simulation
6 |
7 | []
8 | module LeaveVehicleCommand =
9 | /// Command to leave the car or metro.
10 | let get =
11 | { Name = "leave"
12 | Description = "Allows you leave the current vehicle."
13 | Handler =
14 | fun _ ->
15 | Situations.freeRoam |> Effect.apply
16 | Scene.WorldAfterMovement }
17 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Commands/Wait.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Components.Commands
2 |
3 | open Duets.Cli
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 |
8 | []
9 | module WaitCommand =
10 | /// Command which passes time without doing any other change.
11 | let get =
12 | { Name = "wait"
13 | Description = Command.waitDescription
14 | Handler =
15 | (fun _ ->
16 | Wait 1 |> Effect.apply
17 | Scene.World) }
18 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Figlet.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Figlet
3 |
4 | open Spectre.Console
5 |
6 | /// Renders a figlet (see: http://www.figlet.org).
7 | let showFiglet text =
8 | let figlet = FigletText(text).Centered()
9 | figlet.Color <- Color.Blue
10 | AnsiConsole.Write(figlet)
11 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/GameInfo.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.GameInfo
3 |
4 | open Spectre.Console
5 |
6 | ///
7 | /// Shows the version of the game stylized.
8 | ///
9 | /// Current game version
10 | let showGameInfo version =
11 | let gameInfo = $"v{version}"
12 | let styledGameInfo = $"[bold blue dim]{gameInfo}[/]"
13 |
14 | System.Console.SetCursorPosition(
15 | (System.Console.WindowWidth - gameInfo.Length) / 2,
16 | System.Console.CursorTop
17 | )
18 |
19 | AnsiConsole.MarkupLine(styledGameInfo)
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Layout.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Layout
3 |
4 | open Spectre.Console
5 |
6 | /// Clears the whole console buffer and shows an empty screen.
7 | let clearScreen () = System.Console.Clear()
8 |
9 | /// Prints an empty line in the console.
10 | let lineBreak () = AnsiConsole.WriteLine()
11 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Notification.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Notification
3 |
4 | open Duets.Cli.Text
5 | open Spectre.Console
6 |
7 | /// Shows a notification inside of a panel with a bell with the given title
8 | /// and the given body text.
9 | let showNotification (title: string) (text: string) =
10 | let header = PanelHeader(Styles.header $"{Emoji.notification} {title}")
11 |
12 | Panel(
13 | Markup(text),
14 | Header = header,
15 | Border = BoxBorder.Double,
16 | Expand = true,
17 | Padding = Padding(2, 4)
18 | )
19 | |> AnsiConsole.Write
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Post.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Post
3 |
4 | open Duets.Cli.Text
5 | open Duets.Entities
6 | open Spectre.Console
7 |
8 | /// Shows a social network post posted by the given account.
9 | let showPost (account: SocialNetworkAccount) (post: SocialNetworkPost) =
10 | Panel(
11 | Rows(
12 | Markup(
13 | $"@{account.Handle} | {Generic.dateWithDay post.Timestamp}"
14 | |> Styles.faded
15 | ),
16 | Text(post.Text),
17 | Markup($"{Emoji.boost} {post.Reposts}")
18 | ),
19 | Border = BoxBorder.Rounded,
20 | Expand = true
21 | )
22 | |> AnsiConsole.Write
23 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Separator.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Separator
3 |
4 | open Spectre.Console
5 |
6 | /// Renders a line into the screen with an optional text that, if given, shows
7 | /// in the center of the screen.
8 | let showSeparator text =
9 | let rule = Rule().Centered()
10 | rule.Style <- Style.Parse("blue dim")
11 |
12 | match text with
13 | | Some text -> rule.Title <- text
14 | | None -> ()
15 |
16 | AnsiConsole.Write(rule)
17 |
18 | /// Renders an empty line in the screen.
19 | let lineBreak () = AnsiConsole.WriteLine()
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/Tip.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.Tip
3 |
4 | open Duets.Cli.Text
5 | open Spectre.Console
6 |
7 | /// Shows a tip (hint) to the player inside a panel.
8 | let showTip title text =
9 | let header = PanelHeader(Styles.header $"{Emoji.tip} {title}")
10 |
11 | Panel(
12 | Markup(text |> Styles.highlight),
13 | Header = header,
14 | Border = BoxBorder.Rounded,
15 | Expand = false
16 | )
17 | |> AnsiConsole.Write
18 |
19 | /// Shows a list of tips (hints) to the player as a list with a header.
20 | let showTips tips =
21 | "Tips" |> Styles.title |> showMessage
22 | tips |> List.iter (fun tip -> $"- {tip}" |> Styles.hint |> showMessage)
23 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Components/VisualEffects.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Components.VisualEffects
3 |
4 | []
5 | type millisecond
6 |
7 | /// Stops the execution of the CLI for the given amount of seconds. Used to
8 | /// provide a little dramatic pause.
9 | let wait (amount: int) =
10 | System.Threading.Thread.Sleep(amount / 1)
11 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Duets.Cli.fsproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ProjectDebugger
5 |
6 |
7 | Duets.Cli NO SAVE
8 |
9 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Duets.Cli": {
4 | "commandName": "Project"
5 | },
6 | "Duets.Cli NO SAVE": {
7 | "commandName": "Project",
8 | "commandLineArgs": "--no-saving"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.SceneIndex
2 |
3 | open Duets.Entities
4 |
5 | /// Defines whether we should save before exiting the game or not.
6 | []
7 | type ExitMode =
8 | | SaveGame
9 | | SkipSave
10 |
11 | /// Defines the index of all scenes available in the game that can be instantiated.
12 | []
13 | type Scene =
14 | | MainMenu
15 | | Settings
16 | | Cheats
17 | | CharacterCreator
18 | /// World creator needs the playable character from the previous step.
19 | | WorldSelector of Character
20 | // Band creator needs the playable character that was created in the
21 | // previous step and the selected origin city.
22 | | BandCreator of Character * City
23 | /// Skill creator needs the playable character and the band created in
24 | /// the previous steps.
25 | | SkillEditor of Character * CurrentMember * Band * City
26 | /// Shows the world and allows the character to move around and interact
27 | /// with different objects.
28 | | World
29 | /// Shows the world scene after character's movement same as before, but
30 | /// displaying details about the current place.
31 | | WorldAfterMovement
32 | | Phone
33 | // Saves the game and exits.
34 | | Exit of ExitMode
35 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Cheats.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Cheats
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 |
8 | let private leaveCommand =
9 | { Name = "leave"
10 | Description = "Makes you come back to reality"
11 | Handler = fun _ -> Scene.World }
12 |
13 | /// Shows a scene that allows the player to input cheats and debug commands.
14 | let cheatsScene () =
15 | "You're now in cheat mode. Proceed with caution... or not, who cares."
16 | |> Styles.warning
17 | |> showMessage
18 |
19 | let prompt =
20 | $"""{"[[Cheat mode enabled]]" |> Styles.error} What magic trickery are you doing today?"""
21 | |> Styles.prompt
22 |
23 | let commands =
24 | leaveCommand :: ExitCommand.get :: MapCommand.get :: Cheats.Index.all
25 |
26 | commands
27 | |> (@)
28 | [ HelpCommand.createForApp
29 | "cheat mode"
30 | (fun () -> Scene.Cheats)
31 | commands ]
32 | |> showCommandPrompt prompt
33 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/NewGame/WorldSelector.fs:
--------------------------------------------------------------------------------
1 | module rec Duets.Cli.Scenes.NewGame.WorldSelector
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 | open Duets.Simulation
8 |
9 | /// Shows a wizard that allows the player to customize the game world.
10 | let worldSelector character =
11 | showSeparator None
12 | let cities = Queries.World.allCities
13 |
14 | Creator.cityInfo |> showMessage
15 |
16 | let selectedCity =
17 | showChoicePrompt
18 | Creator.cityPrompt
19 | (fun (city: City) -> Generic.cityName city.Id)
20 | cities
21 |
22 | Scene.BandCreator(character, selectedCity)
23 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Bank/DistributeBandFunds.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Bank.DistributeBandFunds
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.Text
7 | open Duets.Entities
8 | open Duets.Simulation.Bands
9 | open Duets.Simulation.Bank.Operations
10 |
11 | let distributeFunds bankApp =
12 | let state = State.get ()
13 |
14 | "This will distribute the amount you choose equally among all band members"
15 | |> Styles.faded
16 | |> showMessage
17 |
18 | let result =
19 | showDecimalPrompt "How much would you like to distribute?"
20 | |> Amount.fromDecimal
21 | |> FundDistribution.distribute state
22 |
23 | match result with
24 | | Ok(effects, totalPerMember) ->
25 | $"You got {totalPerMember |> Styles.money} as your share of the band funds"
26 | |> Styles.success
27 | |> showMessage
28 |
29 | effects |> Effect.applyMultiple
30 | | Error(NotEnoughFunds _) ->
31 | Phone.bankAppTransferNotEnoughFunds |> showMessage
32 |
33 | bankApp ()
34 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Bank/Transfer.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Bank.Transfer
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.Text
7 | open Duets.Entities
8 | open Duets.Simulation.Bank.Operations
9 |
10 | /// Asks for the amount that the user wants to transfer from the two accounts
11 | /// and confirms the transaction.
12 | let transfer bankApp sender receiver =
13 | let amount = showDecimalPrompt (Phone.bankAppTransferAmount receiver)
14 |
15 | if amount > 0m then
16 | transfer (State.get ()) sender receiver (amount * 1m)
17 | |> fun result ->
18 | match result with
19 | | Ok effects -> effects |> List.iter Effect.apply
20 | | Error(NotEnoughFunds _) ->
21 | Phone.bankAppTransferNotEnoughFunds |> showMessage
22 | else
23 | Phone.bankAppTransferNothingTransferred |> showMessage
24 |
25 | bankApp ()
26 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/BnB/BnB.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.BnB.Root
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 |
7 | type private BnBMenuOptions =
8 | | RentPlace
9 | | ListCurrentBookings
10 |
11 | let private textFromOption opt =
12 | match opt with
13 | | RentPlace -> "Rent place"
14 | | ListCurrentBookings -> "List current bookings"
15 |
16 | /// Creates the BnB app, which allows the user to rent places and manage their
17 | /// bookings.
18 | let rec bnbApp () =
19 | let selection =
20 | showOptionalChoicePrompt
21 | "What do you want to do?"
22 | Generic.back
23 | textFromOption
24 | [ RentPlace; ListCurrentBookings ]
25 |
26 | match selection with
27 | | Some RentPlace -> Rent.rent bnbApp
28 | | Some ListCurrentBookings -> List.listAll bnbApp
29 | | None -> Scene.Phone
30 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/ConcertAssistant/ConcertAssistant.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.ConcertAssistant.Root
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 |
7 | type private ConcertMenuOption =
8 | | ScheduleSoloShow
9 | | ScheduleOpeningActShow
10 |
11 | let private textFromOption opt =
12 | match opt with
13 | | ScheduleSoloShow -> "Schedule a show with your band as the headliner"
14 | | ScheduleOpeningActShow -> "Scheduled a show supporting another band"
15 |
16 | let rec concertAssistantApp () =
17 | let selectedChoice =
18 | showOptionalChoicePrompt
19 | Phone.concertAssistantAppPrompt
20 | Generic.nothing
21 | textFromOption
22 | [ ScheduleSoloShow; ScheduleOpeningActShow ]
23 |
24 | match selectedChoice with
25 | | Some ScheduleSoloShow -> SoloShow.scheduleShow concertAssistantApp
26 | | Some ScheduleOpeningActShow -> OpeningAct.scheduleShow concertAssistantApp
27 | | None -> Scene.Phone
28 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Jobs/Jobs.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Jobs.Root
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Duets.Entities
8 | open Duets.Simulation
9 |
10 | type private JobsMenuOption = | FindJob
11 |
12 | let private textFromOption opt =
13 | match opt with
14 | | FindJob -> Phone.findJobOption
15 |
16 | let rec jobsApp () =
17 | let currentCareer = Queries.Career.current (State.get ())
18 |
19 | match currentCareer with
20 | | Some job ->
21 | let currentJobPlace =
22 | job.Location
23 | |> World.Coordinates.toPlaceCoordinates
24 | ||> Queries.World.placeInCityById
25 |
26 | Phone.currentJobDescription job currentJobPlace.Name
27 | | None -> Phone.unemployed
28 | |> showMessage
29 |
30 | lineBreak ()
31 |
32 | let option =
33 | showOptionalChoicePrompt
34 | Phone.optionPrompt
35 | Generic.back
36 | textFromOption
37 | [ FindJob ]
38 |
39 | match option with
40 | | Some FindJob -> FindJob.findJob jobsApp currentCareer
41 | | _ -> Scene.Phone
42 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Commands/Exit.Mastodon.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Scenes.Phone.Apps.Mastodon.Commands
2 |
3 | open Duets.Cli.SceneIndex
4 | open Duets.Cli.Components.Commands
5 |
6 | []
7 | module ExitCommand =
8 | /// Command which returns the user to the phone.
9 | let get =
10 | { Name = "exit"
11 | Description = "Closes the app and returns you to the phone"
12 | Handler = fun _ -> Scene.Phone }
13 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Commands/Post.Mastodon.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Scenes.Phone.Apps.Mastodon.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components
6 | open Duets.Cli.Components.Commands
7 | open Duets.Entities
8 | open Duets.Cli.Text
9 | open Duets.Simulation
10 |
11 | []
12 | module PostCommand =
13 | /// Command which allows the player to post a new toot.
14 | let create account mastodonApp =
15 | { Name = "post"
16 | Description =
17 | $"""Allows you to post something new on your current account"""
18 | Handler =
19 | fun _ ->
20 | let tootText =
21 | showTextPrompt
22 | $"What's on your mind? Will be posted as {Styles.highlight account.Handle}"
23 |
24 | SocialNetworks.Post.toMastodon (State.get ()) account tootText
25 | |> Effect.apply
26 |
27 | mastodonApp () }
28 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Commands/SignUp.Mastodon.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Scenes.Phone.Apps.Mastodon.Commands
2 |
3 | open Duets.Cli.Components.Commands
4 | open Duets.Cli.Scenes.Phone.Apps.Mastodon
5 |
6 | []
7 | module SignUpCommand =
8 | /// Command that allows the player to register another account.
9 | let create mastodonApp =
10 | { Name = "sign up"
11 | Description =
12 | "Allows you to register an account for you or your band, if you haven't done so already"
13 | Handler = fun _ -> SignUp.showSignUpFlow mastodonApp }
14 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Commands/SwitchAccount.Mastodon.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Scenes.Phone.Apps.Mastodon.Commands
2 |
3 | open Duets.Agents
4 | open Duets.Cli
5 | open Duets.Cli.Components.Commands
6 | open Duets.Entities
7 | open Duets.Simulation
8 |
9 | []
10 | module SwitchAccountCommand =
11 | /// Command to switch between the character and the band's account.
12 | let create mastodonApp =
13 | { Name = "switch account"
14 | Description =
15 | "Switches between your character's and your band's account"
16 | Handler =
17 | fun _ ->
18 | SocialNetworks.Account.switch
19 | (State.get ())
20 | SocialNetworkKey.Mastodon
21 | |> Effect.applyMultiple
22 |
23 | mastodonApp () }
24 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Commands/Timeline.Mastodon.Command.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Scenes.Phone.Apps.Mastodon.Commands
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.Components.Commands
5 | open Duets.Entities
6 |
7 | []
8 | module TimelineCommand =
9 | /// Command which shows the current timeline to the player.
10 | let create account mastodonApp =
11 | { Name = "timeline"
12 | Description = "Shows all the toots that you previously posted"
13 | Handler =
14 | fun _ ->
15 | if List.isEmpty account.Posts then
16 | "Nothing to show" |> showMessage
17 | else
18 | account.Posts |> List.iter (showPost account)
19 |
20 | mastodonApp () }
21 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Mastodon/Mastodon.fs:
--------------------------------------------------------------------------------
1 | module rec Duets.Cli.Scenes.Phone.Apps.Mastodon.Root
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components.Commands
5 | open Duets.Cli.Text
6 | open Duets.Cli.Components
7 | open Duets.Entities
8 | open Duets.Simulation
9 | open Duets.Cli.Scenes.Phone.Apps
10 |
11 | let rec mastodonApp () =
12 | let currentAccount =
13 | Queries.SocialNetworks.currentAccount
14 | (State.get ())
15 | SocialNetworkKey.Mastodon
16 |
17 | match currentAccount with
18 | | Some account -> showPrompt account
19 | | None -> SignUp.showInitialSignUpFlow mastodonApp
20 |
21 | and private showPrompt account =
22 | let promptText =
23 | $"""{Emoji.mastodon} {$"@{account.Handle}" |> Styles.highlight} | {$"{Styles.number account.Followers} followers" |> Styles.Level.good}"""
24 |
25 | let appCommands =
26 | [ Mastodon.Commands.TimelineCommand.create account mastodonApp
27 | Mastodon.Commands.SignUpCommand.create mastodonApp
28 | Mastodon.Commands.SwitchAccountCommand.create mastodonApp
29 | Mastodon.Commands.PostCommand.create account mastodonApp
30 | Mastodon.Commands.ExitCommand.get ]
31 |
32 | appCommands
33 | |> (@) [ HelpCommand.createForApp "Mastodon" mastodonApp appCommands ]
34 | |> showCommandPrompt promptText
35 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Statistics/RelationshipsStatistics.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Statistics.Relationships
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.Text
6 | open Duets.Common
7 | open Duets.Entities
8 | open Duets.Simulation
9 |
10 | let rec relationshipsStatisticsSubScene statisticsApp =
11 | let state = State.get ()
12 | let relationships = Queries.Relationship.all state |> List.ofMapValues
13 |
14 | let tableColumns =
15 | [ Styles.header "Name"
16 | Styles.header "Relationship type"
17 | Styles.header "Level" ]
18 |
19 | let tableRows =
20 | relationships
21 | |> List.map (fun relationship ->
22 | let npc = Queries.Characters.find state relationship.Character
23 |
24 | [ Styles.person npc.Name
25 | Social.relationshipType relationship.RelationshipType
26 | |> Styles.highlight
27 | $"{relationship.Level |> Styles.Level.from}%%" ])
28 |
29 | showTable tableColumns tableRows
30 |
31 | statisticsApp ()
32 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Statistics/ReviewStatistics.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Statistics.AlbumReviews
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.Text
6 | open Duets.Entities
7 | open Duets.Simulation.Queries
8 |
9 | let rec reviewsStatisticsSubScene statisticsApp =
10 | let state = State.get ()
11 | let band = Bands.currentBand state
12 |
13 | let releases = Albums.releasedByBand state band.Id
14 |
15 | if List.isEmpty releases then
16 | Phone.statisticsAppAlbumNoEntries |> showMessage
17 | statisticsApp ()
18 | else
19 | showAlbumSelection statisticsApp releases
20 |
21 | and private showAlbumSelection statisticsApp albums =
22 | let selection =
23 | showOptionalChoicePrompt
24 | "Which album do you want to see reviews for?"
25 | Generic.backToPhone
26 | (_.Album.Name)
27 | albums
28 |
29 | match selection with
30 | | Some album ->
31 | if List.isEmpty album.Reviews then
32 | "No one really cared about the album that much to write a review"
33 | |> Styles.error
34 | |> showMessage
35 | else
36 | showReviews album
37 |
38 | reviewsStatisticsSubScene statisticsApp
39 | | None -> statisticsApp ()
40 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Statistics/Statistics.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Statistics.Root
2 |
3 | open Duets.Cli.Components
4 | open Duets.Cli.SceneIndex
5 | open Duets.Cli.Text
6 |
7 | type private StatisticsOption =
8 | | Band
9 | | Albums
10 | | Reviews
11 | | Relationships
12 |
13 | let private textFromOption opt =
14 | match opt with
15 | | Band -> "Band statistics"
16 | | Albums -> "Album statistics"
17 | | Reviews -> "Album reviews"
18 | | Relationships -> "Relationships"
19 |
20 | let rec statisticsApp () =
21 | let selectedChoice =
22 | showOptionalChoicePrompt
23 | Phone.statisticsAppSectionPrompt
24 | Generic.backToPhone
25 | textFromOption
26 | [ Band; Albums; Reviews; Relationships ]
27 |
28 | match selectedChoice with
29 | | Some Band -> Band.bandStatisticsSubScene statisticsApp
30 | | Some Albums -> Albums.albumsStatisticsSubScene statisticsApp
31 | | Some Reviews -> AlbumReviews.reviewsStatisticsSubScene statisticsApp
32 | | Some Relationships ->
33 | Relationships.relationshipsStatisticsSubScene statisticsApp
34 | | None -> Scene.Phone
35 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Scenes/Phone/Apps/Weather/Weather.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Scenes.Phone.Apps.Weather.Root
2 |
3 | open Duets.Agents
4 | open Duets.Cli.Components
5 | open Duets.Cli.SceneIndex
6 | open Duets.Cli.Text
7 | open Duets.Entities
8 | open Duets.Simulation
9 |
10 | let private weatherIcon weatherCondition =
11 | match weatherCondition with
12 | | WeatherCondition.Sunny -> "☀️"
13 | | WeatherCondition.Cloudy -> "☁️"
14 | | WeatherCondition.Rainy -> "🌧️"
15 | | WeatherCondition.Stormy -> "⛈️"
16 | | WeatherCondition.Snowy -> "❄️"
17 |
18 | let private weatherDescription weatherCondition =
19 | match weatherCondition with
20 | | WeatherCondition.Sunny -> "Sunny"
21 | | WeatherCondition.Cloudy -> "Cloudy"
22 | | WeatherCondition.Rainy -> "Rainy"
23 | | WeatherCondition.Stormy -> "Stormy"
24 | | WeatherCondition.Snowy -> "Snowy"
25 |
26 | let weatherApp () =
27 | let state = State.get ()
28 | let currentCity = Queries.World.currentCity state
29 | let currentWeather = Queries.World.currentWeather state
30 |
31 | let icon = weatherIcon currentWeather
32 | let description = weatherDescription currentWeather
33 |
34 | Phone.weatherAppTitle |> Styles.header |> showMessage
35 | Phone.weatherAppContent currentCity.Id icon description |> showMessage
36 |
37 | showContinuationPrompt ()
38 |
39 | Scene.Phone
40 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Character.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Text.Character
2 |
3 | open Duets.Entities
4 |
5 | let attributeName attr =
6 | match attr with
7 | | CharacterAttribute.Drunkenness -> "Drunkenness"
8 | | CharacterAttribute.Energy -> "Energy"
9 | | CharacterAttribute.Fame -> "Fame"
10 | | CharacterAttribute.Health -> "Health"
11 | | CharacterAttribute.Hunger -> "Hunger"
12 | | CharacterAttribute.Mood -> "Mood"
13 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Date.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Text.Date
2 |
3 | open Duets.Entities
4 |
5 | let seasonName (season: Season) =
6 | match season with
7 | | Spring -> "Spring"
8 | | Summer -> "Summer"
9 | | Autumn -> "Autumn"
10 | | Winter -> "Winter"
11 |
12 | /// Formats a date to `Season, Year` format.
13 | let seasonYear (date: Date) =
14 | $"{seasonName date.Season}, {date.Year}"
15 |
16 | /// Formats a date to `Day d of Season, Year` format.
17 | let simple (date: Date) = $"Day {date.Day} of {seasonYear date}"
18 |
19 | /// Formats a date to the dd/mm/yyyy format and adds the name of the day in
20 | /// the beginning.
21 | let withDayName (date: Date) =
22 | $"{Calendar.Query.dayOfWeek date}, {simple date}"
23 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Events.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Text.Events
3 |
4 | let healthDepletedFirst =
5 | Styles.danger "You start to feel lightheaded and your vision begins to blur"
6 |
7 | let healthDepletedSecond =
8 | Styles.danger
9 | "The background noise starts to fade into the background and a buzz grows inside your ear..."
10 |
11 | let hospitalized =
12 | Styles.information
13 | "You wake up in the hospital a week later. Your head hurts a bit, but other than that it seems like you will make it this time"
14 |
15 | let feelingTipsy =
16 | Styles.information
17 | "You feel a bit tipsy, your eyes start to lower a bit and you seem to have a fixed smile on your face"
18 |
19 | let feelingDrunk =
20 | Styles.information
21 | "You feel a bit drunk, doing stuff seems a bit more difficult than before"
22 |
23 | let feelingReallyDrunk =
24 | Styles.danger
25 | "You feel really drunk. Your eyes are blurry and your legs don't seem to be able to follow the same pattern. A part of your body asks you to stop, but the other one wants a bit more fun..."
26 |
27 | let soberingTipsy =
28 | Styles.information "You're feeling much better now, just slightly tipsy"
29 |
30 | let soberingDrunk =
31 | Styles.information
32 | "You're starting to get sober, still feeling really drunk, but better than before"
33 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/MainMenu.fs:
--------------------------------------------------------------------------------
1 | []
2 | module Duets.Cli.Text.MainMenu
3 |
4 | let incompatibleSavegame message =
5 | Styles.error
6 | $"""Your savegame is incompatible or malformed and was ignored. Error:
7 | {message}"""
8 |
9 | let prompt = "Select an option to begin"
10 | let newGame = "New game"
11 | let loadGame = "Load game"
12 | let settings = "Settings"
13 | let exit = Styles.faded "Exit"
14 |
15 | let savegameNotAvailable =
16 | Styles.error "No savegame available. Create a new game"
17 |
18 | let newGameReplacePrompt =
19 | Styles.danger
20 | "Creating a new game will replace your current savegame and all the progress will be lost, are you sure?"
21 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Prompts/CommonPrompts.fs:
--------------------------------------------------------------------------------
1 | namespace Duets.Cli.Text.Prompts
2 |
3 | open Duets.Common
4 | open Duets.Entities
5 |
6 | []
7 | module Common =
8 | /// Creates a prompt that improves the response quality of the language model.
9 | /// Currently tuned for Gemma 3, which requires explicit turn markers to get
10 | /// anything useful out of it.
11 | let internal createPrompt prompt =
12 | $"""
13 | user
14 | {prompt}
15 |
16 | model
17 | """
18 |
19 | let internal itemNameForPrompt item =
20 | let mainProperty = item.Properties |> List.head
21 |
22 | match mainProperty with
23 | | Key(EntranceCard _) -> "entrance card"
24 | | Rideable(RideableItem.Car _) -> $"{item.Brand} {item.Name}"
25 | | _ -> item.Name |> String.lowercase
26 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Social.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Text.Social
2 |
3 | open Duets.Entities
4 |
5 | let actionPrompt date dayMoment attributes npc relationshipLevel =
6 | $"""{Generic.infoBar date dayMoment attributes}
7 | {Emoji.socializing} Talking with {npc.Name |> Styles.person} | {Emoji.relationshipLevel} {relationshipLevel}
8 | What do you want to do?"""
9 | |> Styles.prompt
10 |
11 | let relationshipType =
12 | function
13 | | Friend -> "Friend"
14 | | Bandmate -> "Bandmate"
15 |
16 | let npcSaysPrefix (npcName: string) = $"{Styles.person npcName}: "
17 |
18 | let npcSays npcName text =
19 | $"{npcSaysPrefix npcName}{Styles.dialog text}"
20 |
--------------------------------------------------------------------------------
/src/Duets.Cli/Text/Songs.fs:
--------------------------------------------------------------------------------
1 | module Duets.Cli.Text.Songs
2 |
3 | open Duets.Entities
4 |
5 | /// Formats a given length as minutes:seconds.
6 | let length (l: Length) = $"{l.Minutes}:{l.Seconds}"
7 |
--------------------------------------------------------------------------------
/src/Duets.Common/Duets.Common.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(DotnetVersion)
4 | true
5 | true
6 | Common
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Duets.Common/Func.fs:
--------------------------------------------------------------------------------
1 | module Duets.Common.Func
2 |
3 | /// Transforms an F# function into a System.Func.
4 | let toFunc<'a, 'b> f = System.Func<'a, 'b>(f)
5 |
6 | /// Wraps a value in a function that ignores its input and returns the value.
7 | let toConst<'a> (value: 'a) _ = value
8 |
--------------------------------------------------------------------------------
/src/Duets.Common/Map.fs:
--------------------------------------------------------------------------------
1 | module Duets.Common.Map
2 |
3 | open Aether
4 |
5 | /// Prism to a value associated with a key in a map. Similar as Aether's
6 | /// built in but this one always introduces the key in the map regardless
7 | /// whether it already exists or not.
8 | let key_ (k: 'k) : Prism