Add subplots
authorTheSaminator <TheSaminator@users.noreply.github.com>
Tue, 7 Jun 2022 23:18:14 +0000 (19:18 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Tue, 7 Jun 2022 23:18:14 +0000 (19:18 -0400)
18 files changed:
src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/data/admiralty/ship_names.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt
src/commonMain/kotlin/starshipfights/game/game_packet.kt
src/commonMain/kotlin/starshipfights/game/game_state.kt
src/commonMain/kotlin/starshipfights/game/game_subplots.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/matchmaking.kt
src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/util.kt
src/jsMain/kotlin/starshipfights/game/client_game.kt
src/jsMain/kotlin/starshipfights/game/client_training.kt
src/jsMain/kotlin/starshipfights/game/game_ui.kt
src/jsMain/kotlin/starshipfights/game/popup.kt
src/jsMain/resources/style.css
src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt [deleted file]
src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt [deleted file]
src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt
src/jvmMain/kotlin/starshipfights/game/server_game.kt

diff --git a/src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt b/src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt
new file mode 100644 (file)
index 0000000..520829d
--- /dev/null
@@ -0,0 +1,873 @@
+package starshipfights.data.admiralty
+
+import starshipfights.game.Faction
+import kotlin.random.Random
+
+enum class AdmiralNameFlavor {
+       MECHYRDIA, TYLA, CALIBOR, OLYMPIA, // Mechyrdia-aligned
+       DUTCH, // NdRC-aliged
+       NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI, // Masra Draetsen-aligned
+       FULKREYKK, // Isarnareykk-aligned
+       AMERICAN, HISPANIC_AMERICAN; // Vestigium-aligned
+       
+       val displayName: String
+               get() = when (this) {
+                       MECHYRDIA -> "Mechyrdian"
+                       TYLA -> "Tylan"
+                       CALIBOR -> "Caliborese"
+                       OLYMPIA -> "Olympian"
+                       DUTCH -> "Dutch"
+                       NORTHERN_DIADOCHI -> "Northern Diadochi"
+                       SOUTHERN_DIADOCHI -> "Southern Diadochi"
+                       FULKREYKK -> "Thedish"
+                       AMERICAN -> "American"
+                       HISPANIC_AMERICAN -> "Hispanic-American"
+               }
+       
+       companion object {
+               fun forFaction(faction: Faction) = when (faction) {
+                       Faction.MECHYRDIA -> setOf(MECHYRDIA, TYLA, CALIBOR, OLYMPIA, DUTCH)
+                       Faction.NDRC -> setOf(DUTCH)
+                       Faction.MASRA_DRAETSEN -> setOf(CALIBOR, NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI)
+                       Faction.FELINAE_FELICES -> setOf(OLYMPIA)
+                       Faction.ISARNAREYKK -> setOf(FULKREYKK)
+                       Faction.VESTIGIUM -> setOf(AMERICAN, HISPANIC_AMERICAN)
+               }
+       }
+}
+
+object AdmiralNames {
+       // PERSONAL NAME to PATRONYMIC
+       private val mechyrdianMaleNames: List<Pair<String, String>> = listOf(
+               "Marc" to "Marcówič",
+               "Anton" to "Antonówič",
+               "Bjarnarð" to "Bjarnarðówič",
+               "Carl" to "Carlówič",
+               "Þjutarix" to "Þjutarigówič",
+               "Friðurix" to "Friðurigówič",
+               "Iwan" to "Iwanówič",
+               "Wladimer" to "Wladimerówič",
+               "Giulius" to "Giuliówič",
+               "Nicólei" to "Nicóleiówič",
+               "Þjódor" to "Þjóderówič",
+               "Sigismund" to "Sigismundówič",
+               "Stefan" to "Stefanówič",
+               "Wilhelm" to "Wilhelmówič",
+               "Giórgj" to "Giórgiówič"
+       )
+       
+       // PERSONAL NAME to MATRONYMIC
+       private val mechyrdianFemaleNames: List<Pair<String, String>> = listOf(
+               "Octavia" to "Octaviówca",
+               "Annica" to "Annicówca",
+               "Astrið" to "Astriðówca",
+               "Caþarin" to "Caþarinówca",
+               "Signi" to "Signówca",
+               "Erica" to "Ericówca",
+               "Fréja" to "Fréjówca",
+               "Hilda" to "Hildówca",
+               "Žanna" to "Žannówca",
+               "Xenia" to "Xeniówca",
+               "Carina" to "Carinówca",
+               "Giadwiga" to "Giadwigówca",
+               "Ženia" to "Ženiówca"
+       )
+       
+       private val mechyrdianFamilyNames: List<Pair<String, String>> = listOf(
+               "Alexandrów",
+               "Antonów",
+               "Pogdanów",
+               "Hrusčjów",
+               "Caísarów",
+               "Carolów",
+               "Sócolów",
+               "Romanów",
+               "Nemeciów",
+               "Pjótrów",
+               "Brutów",
+               "Augustów",
+               "Calašniców",
+               "Anželów",
+               "Sigmarów",
+               "Dróganów",
+               "Coroljów",
+               "Wlasów"
+       ).map { it to "${it}a" }
+       
+       private fun randomMechyrdianName(isFemale: Boolean) = if (isFemale)
+               mechyrdianFemaleNames.random().first + " " + mechyrdianFemaleNames.random().second + " " + mechyrdianFamilyNames.random().second
+       else
+               mechyrdianMaleNames.random().first + " " + mechyrdianMaleNames.random().second + " " + mechyrdianFamilyNames.random().first
+       
+       private val tylanMaleNames = listOf(
+               "Althanar" to "Althanas",
+               "Aurans" to "Aurantes",
+               "Bochra" to "Bochranes",
+               "Chshaejar" to "Chshaejas",
+               "Hjofvachi" to "Hjovachines",
+               "Koldimar" to "Koldimas",
+               "Kor" to "Kores",
+               "Ljomas" to "Ljomates",
+               "Shajel" to "Shajel",
+               "Shokar" to "Shokas",
+               "Tolavajel" to "Tolavajel",
+               "Voskar" to "Voskas",
+       )
+       
+       private val tylanFemaleNames = listOf(
+               "Althe" to "Althenes",
+               "Anaseil" to "Anaseil",
+               "Asetbur" to "Asetbus",
+               "Atautha" to "Atauthas",
+               "Aurantia" to "Aurantias",
+               "Ilasheva" to "Ilashevas",
+               "Kalora" to "Kaloras",
+               "Kotolva" to "Kotolvas",
+               "Psekna" to "Pseknas",
+               "Shenera" to "Sheneras",
+               "Reoka" to "Reokas",
+               "Velga" to "Velgas",
+       )
+       
+       private val tylanFamilyNames = listOf(
+               "Kalevkar" to "Kalevka",
+               "Merku" to "Merkussa",
+               "Telet" to "Telet",
+               "Eutokar" to "Eutoka",
+               "Vsocha" to "Vsochessa",
+               "Vilar" to "Vilakauva",
+               "Nikasrar" to "Nika",
+               "Vlegamakar" to "Vlegamaka",
+               "Vtokassar" to "Vtoka",
+               "Theiar" to "Theia",
+               "Aretar" to "Areta",
+               "Derkas" to "Derkata",
+               "Vinsennas" to "Vinsenatta",
+               "Kleio" to "Kleona"
+       )
+       
+       // Tylans use matronymics for both sons and daughters
+       private fun randomTylanName(isFemale: Boolean) = if (isFemale)
+               tylanFemaleNames.random().first + " " + tylanFemaleNames.random().second + "-Nahra " + tylanFamilyNames.random().second
+       else
+               tylanMaleNames.random().first + " " + tylanFemaleNames.random().second + "-Nensar " + tylanFamilyNames.random().first
+       
+       private val caliboreseNames = listOf(
+               "Jathee",
+               "Muly",
+               "Simoh",
+               "Laka",
+               "Foryn",
+               "Duxio",
+               "Xirio",
+               "Surmy",
+               "Datarme",
+               "Cloren",
+               "Tared",
+               "Quiliot",
+               "Attiol",
+               "Quarree",
+               "Guil",
+               "Miro",
+               "Yryys",
+               "Zarx",
+               "Karm",
+               "Mreek",
+               "Dulyy",
+               "Quorqui",
+               "Dreminor",
+               "Samitu",
+               "Lurmak",
+               "Quashi",
+               "Barsyn",
+               "Rymyo",
+               "Soli",
+               "Ickart",
+               "Woom",
+               "Qurquy",
+               "Ymiro",
+               "Rosiliq",
+               "Xant",
+               "Xateen",
+               "Mssly",
+               "Vixie",
+               "Quelynn",
+               "Plly",
+               "Tessy",
+               "Veekah",
+               "Quett",
+               "Xezeez",
+               "Xyph",
+               "Jixi",
+               "Jeekie",
+               "Meelen",
+               "Rasah",
+               "Reteeshy",
+               "Xinchie",
+               "Zae",
+               "Ziggy",
+               "Wurikah",
+               "Loppie",
+               "Tymma",
+               "Reely",
+               "Yjutee",
+               "Len",
+               "Vixirat",
+               "Xumie",
+               "Xilly",
+               "Liwwy",
+               "Gancee",
+               "Pamah",
+               "Zeryll",
+               "Luteet",
+               "Qusseet",
+               "Alixika",
+               "Sepirah",
+               "Luttrah",
+               "Aramynn",
+               "Laxerynn",
+               "Murylyt",
+               "Quarapyt",
+               "Tormiray",
+               "Daromynn",
+               "Zuleerynn",
+               "Quarimat",
+               "Dormaquazi",
+               "Tullequazi",
+               "Aleeray",
+               "Eppiquit",
+               "Wittirynn",
+               "Semiokolipan",
+               "Sosopurr",
+               "Quamixit",
+               "Croffet",
+               "Xaalit",
+               "Xemiolyt"
+       )
+       
+       private val caliboreseVowels = "aeiouy".toSet()
+       private fun randomCaliboreseName(isFemale: Boolean) = caliboreseNames.filter {
+               it.length < 8 && (isFemale == (it.last() in caliboreseVowels))
+       }.random() + " " + caliboreseNames.filter { it.length > 7 }.random()
+       
+       private val latinMaleCommonPraenomina = listOf(
+               "Gaius",
+               "Lucius",
+               "Marcus",
+       )
+       
+       private val latinMaleUncommonPraenomina = listOf(
+               "Publius",
+               "Quintus",
+               "Titus",
+               "Gnaeus"
+       )
+       
+       private val latinMaleRarePraenomina = listOf(
+               "Aulus",
+               "Spurius",
+               "Tiberius",
+               "Servius",
+               "Hostus"
+       )
+       
+       private val latinFemaleCommonPraenomina = listOf(
+               "Gaia",
+               "Lucia",
+               "Marcia",
+       )
+       
+       private val latinFemaleUncommonPraenomina = listOf(
+               "Prima",
+               "Secunda",
+               "Tertia",
+               "Quarta",
+               "Quinta",
+               "Sexta",
+               "Septima",
+               "Octavia",
+               "Nona",
+               "Decima"
+       )
+       
+       private val latinFemaleRarePraenomina = listOf(
+               "Caesula",
+               "Titia",
+               "Tiberia",
+               "Tanaquil"
+       )
+       
+       private val latinNominaGentilica = listOf(
+               "Aelius" to "Aelia",
+               "Aternius" to "Aternia",
+               "Caecilius" to "Caecilia",
+               "Cassius" to "Cassia",
+               "Claudius" to "Claudia",
+               "Cornelius" to "Cornelia",
+               "Calpurnius" to "Calpurnia",
+               "Fabius" to "Fabia",
+               "Flavius" to "Flavia",
+               "Fulvius" to "Fulvia",
+               "Haterius" to "Hateria",
+               "Hostilius" to "Hostilia",
+               "Iulius" to "Iulia",
+               "Iunius" to "Iunia",
+               "Iuventius" to "Iuventia",
+               "Lavinius" to "Lavinia",
+               "Licinius" to "Licinia",
+               "Marius" to "Maria",
+               "Octavius" to "Octavia",
+               "Pompeius" to "Pompeia",
+               "Porcius" to "Porcia",
+               "Salvius" to "Salvia",
+               "Sempronius" to "Sempronia",
+               "Spurius" to "Spuria",
+               "Terentius" to "Terentia",
+               "Tullius" to "Tullia",
+               "Ulpius" to "Ulpia",
+               "Valerius" to "Valeria"
+       )
+       
+       private val latinCognomina = listOf(
+               "Agricola" to "Agricola",
+               "Agrippa" to "Agrippina",
+               "Aquilinus" to "Aquilina",
+               "Balbus" to "Balba",
+               "Bibulus" to "Bibula",
+               "Bucco" to "Bucco",
+               "Caecus" to "Caeca",
+               "Calidus" to "Calida",
+               "Catilina" to "Catilina",
+               "Catulus" to "Catula",
+               "Crassus" to "Crassa",
+               "Crispus" to "Crispa",
+               "Drusus" to "Drusilla",
+               "Flaccus" to "Flacca",
+               "Gracchus" to "Graccha",
+               "Laevinus" to "Laevina",
+               "Lanius" to "Lania",
+               "Lepidus" to "Lepida",
+               "Lucullus" to "Luculla",
+               "Marcellus" to "Marcella",
+               "Metellus" to "Metella",
+               "Nasica" to "Nasica",
+               "Nerva" to "Nerva",
+               "Paullus" to "Paulla",
+               "Piso" to "Piso",
+               "Priscus" to "Prisca",
+               "Publicola" to "Publicola",
+               "Pulcher" to "Pulchra",
+               "Regulus" to "Regula",
+               "Rufus" to "Rufa",
+               "Scaevola" to "Scaevola",
+               "Severus" to "Severa",
+               "Structus" to "Structa",
+               "Taurus" to "Taura",
+               "Varro" to "Varro",
+               "Vitulus" to "Vitula"
+       )
+       
+       private fun randomLatinPraenomen(isFemale: Boolean) = when {
+               Random.nextBoolean() -> if (isFemale) latinFemaleCommonPraenomina else latinMaleCommonPraenomina
+               Random.nextInt(3) > 0 -> if (isFemale) latinFemaleUncommonPraenomina else latinMaleUncommonPraenomina
+               else -> if (isFemale) latinFemaleRarePraenomina else latinMaleRarePraenomina
+       }.random()
+       
+       private fun randomLatinName(isFemale: Boolean) = randomLatinPraenomen(isFemale) + " " + latinNominaGentilica.random().let { (m, f) -> if (isFemale) f else m } + " " + latinCognomina.random().let { (m, f) -> if (isFemale) f else m }
+       
+       private val dutchMaleNames = listOf(
+               "Aalderik",
+               "Andreas",
+               "Boudewijn",
+               "Bruno",
+               "Christiaan",
+               "Cornelius",
+               "Darnath",
+               "Dirk",
+               "Eren",
+               "Erwin",
+               "Frederik",
+               "Gerlach",
+               "Helbrant",
+               "Helbrecht",
+               "Hendrik",
+               "Jakob",
+               "Jochem",
+               "Joris",
+               "Koenraad",
+               "Koorland",
+               "Leopold",
+               "Lodewijk",
+               "Maarten",
+               "Michel",
+               "Niels",
+               "Pieter",
+               "Renaat",
+               "Rogal",
+               "Ruben",
+               "Sebastiaan",
+               "Sigismund",
+               "Sjaak",
+               "Tobias",
+               "Valentijn",
+               "Wiebrand",
+       )
+       
+       private val dutchFemaleNames = listOf(
+               "Adelwijn",
+               "Amberlij",
+               "Annika",
+               "Arete",
+               "Eva",
+               "Gerda",
+               "Helga",
+               "Ida",
+               "Irene",
+               "Jacqueline",
+               "Josefien",
+               "Juliana",
+               "Katharijne",
+               "Lore",
+               "Margriet",
+               "Maximilia",
+               "Meike",
+               "Nora",
+               "Rebeka",
+               "Sara",
+               "Vera",
+               "Wilhelmina",
+       )
+       
+       private val dutchMerchantHouses = listOf(
+               "Venetho",
+               "Luibeck",
+               "Birka",
+               "Heiðabýr",
+               "Rostok",
+               "Guistrov",
+               "Schverin",
+               "Koeln",
+               "Bruigge",
+               "Reval",
+               "Elbing",
+               "Dorpat",
+               "Stralsund",
+               "Mijdeborg",
+               "Breslaw",
+               "Dortmund",
+               "Antwerp",
+               "Falsterbo",
+               "Zwolle",
+               "Buchtehud",
+               "Bremen",
+               "Zutphen",
+               "Kampen",
+               "Grunn",
+               "Deventer",
+               "Wismer",
+               "Luinenburg",
+               
+               "Jager",
+               "Jastobaal",
+               "Varonius",
+               "Kupferberg",
+               "Dijn",
+               "Umboldt",
+               "Phalomor",
+               "Drijk",
+               "d'Wain",
+               "du Languille",
+               "Horstein",
+               "Jerulas",
+               "Kendar",
+               "Castellan",
+               "d'Aniasie",
+               "Gerrit",
+               "Hoed",
+               "lo Pan",
+               "Marchandrij",
+               "d'Aquairre",
+               "Terozzante",
+               "d'Argovon",
+               "de Monde",
+               "Paillender",
+               "Holstijn",
+               "d'Imperia",
+               "Borodin",
+               "Agranozza",
+               "d'Ortise",
+               "Ijzerhoorn",
+               "Dremel",
+               "Hinckel",
+               "Vuigens",
+               "Drazen",
+               "Marburg",
+               "Xardt",
+               "Lijze",
+               "Gerlach",
+               "Doorn",
+               "d'Arquebus",
+               "Alderic",
+               "Vogen"
+       )
+       
+       private fun randomDutchName(isFemale: Boolean) = (if (isFemale) dutchFemaleNames else dutchMaleNames).random() + " van " + dutchMerchantHouses.random()
+       
+       private val diadochiMaleNames = listOf(
+               "Oqatai",
+               "Amogus",
+               "Nerokhan",
+               "Choghor",
+               "Aghonei",
+               "Martaq",
+               "Qaran",
+               "Khargh",
+               "Qolkhu",
+               "Ghauran",
+               "Woriv",
+               "Vorcha",
+               "Chagatai",
+               "Neghvar",
+               "Qitinga",
+               "Jimpaq",
+               "Bivat",
+               "Durash",
+               "Elifas",
+               "Ogus",
+               "Yuli",
+               "Saret",
+               "Mher",
+               "Tyver",
+               "Ghraq",
+               "Niran",
+               "Galik"
+       )
+       
+       private val diadochiFemaleNames = listOf(
+               "Lursha",
+               "Jamoqena",
+               "Lokoria",
+               "Iekuna",
+               "Shara",
+               "Etugen",
+               "Maral",
+               "Temuln",
+               "Akhensari",
+               "Khadagan",
+               "Gherelma",
+               "Shechen",
+               "Althani",
+               "Tzyrina",
+               "Daghasi",
+               "Kloya",
+       )
+       
+       private val northernDiadochiEpithetParts = listOf(
+               "Skull",
+               "Blood",
+               "Death",
+               "Claw",
+               "Doom",
+               "Dread",
+               "Soul",
+               "Spirit",
+               "Hell",
+               "Dread",
+               "Bale",
+               "Fire",
+               "Fist",
+               "Bear",
+               "Pyre",
+               "Dark",
+               "Vile",
+               "Heart",
+               "Murder",
+               "Gore",
+               "Daemon",
+               "Talon",
+       )
+       
+       private fun randomNorthernDiadochiName(isFemale: Boolean) = (if (isFemale) diadochiFemaleNames else diadochiMaleNames).random() + " " + northernDiadochiEpithetParts.random() + northernDiadochiEpithetParts.random().lowercase()
+       
+       private val southernDiadochiClans = listOf(
+               "Arkai",
+               "Avado",
+               "Djahhim",
+               "Khankhen",
+               "Porok",
+               "Miras",
+               "Terok",
+               "Empok",
+               "Noragh",
+               "Nuunian",
+               "Soung",
+               "Akhero",
+               "Qozaq",
+               "Kherus",
+               "Axina",
+               "Ghaizas",
+               "Saxha",
+               "Meshu",
+               "Khopesh",
+               "Qitemar",
+               "Vang",
+               "Lugal",
+               "Galla",
+               "Hheka",
+               "Nesut",
+               "Koquon",
+               "Molekh"
+       )
+       
+       private fun randomSouthernDiadochiClan() = when {
+               Random.nextInt(5) == 0 -> southernDiadochiClans.random() + "-" + southernDiadochiClans.random()
+               else -> southernDiadochiClans.random()
+       }
+       
+       private fun randomSouthernDiadochiName(isFemale: Boolean) = (if (isFemale) diadochiFemaleNames else diadochiMaleNames).random() + (if (isFemale && Random.nextBoolean()) " ka-" else " am-") + diadochiMaleNames.random() + " " + randomSouthernDiadochiClan()
+       
+       private val thedishMaleNames = listOf(
+               "Praethoris",
+               "Severus",
+               "Augast",
+               "Dagobar",
+               "Vrankenn",
+               "Kandar",
+               "Kleon",
+               "Glaius",
+               "Karul",
+               "Ylai",
+               "Toval",
+               "Ivon",
+               "Belis",
+               "Jorh",
+               "Svar",
+               "Alaric",
+       )
+       
+       private val thedishFemaleNames = listOf(
+               "Serna",
+               "Veleska",
+               "Ielga",
+               "Glae",
+               "Rova",
+               "Ylia",
+               "Galera",
+               "Nerys",
+               "Veleer",
+               "Karuleyn",
+               "Amberli",
+               "Alysia",
+               "Lenera",
+               "Demeter",
+       )
+       
+       private val thedishSurnames = listOf(
+               "Kassck",
+               "Orsh",
+               "Falk",
+               "Khorr",
+               "Vaskoman",
+               "Vholkazk",
+               "Brekoryn",
+               "Lorus",
+               "Karnas",
+               "Hathar",
+               "Takan",
+               "Pertona",
+               "Tefran",
+               "Arvi",
+               "Galvus",
+               "Voss",
+               "Mandanof",
+               "Ursali",
+               "Vytunn",
+               "Quesrinn",
+       )
+       
+       private fun randomThedishName(isFemale: Boolean) = (if (isFemale) thedishFemaleNames else thedishMaleNames).random() + " " + thedishSurnames.random()
+       
+       private val americanMaleNames = listOf(
+               "George",
+               "John",
+               "Thomas",
+               "James",
+               "Quincy",
+               "Andrew",
+               "Martin",
+               "William",
+               "Henry",
+               "James",
+               "Zachary",
+               "Millard",
+               "Franklin",
+               "Abraham",
+               "Ulysses",
+               "Rutherford",
+               "Chester",
+               "Grover",
+               "Benjamin",
+               "Theodore",
+               "Warren",
+               "Calvin",
+               "Herbert",
+               "Harry",
+               "Dwight",
+               "Lyndon",
+               "Richard",
+               "Dick",
+               "Gerald",
+               "Jimmy",
+               "Ronald",
+               "Donald"
+       )
+       
+       private val americanFemaleNames = listOf(
+               "Martha",
+               "Abigail",
+               "Elizabeth",
+               "Louisa",
+               "Emily",
+               "Sarah",
+               "Anna",
+               "Jane",
+               "Julia",
+               "Margaret",
+               "Harriet",
+               "Mary",
+               "Lucy",
+               "Rose",
+               "Caroline",
+               "Ida",
+               "Helen",
+               "Grace",
+               "Jacqueline",
+               "Thelma",
+               "Eleanor",
+               "Nancy",
+               "Barbara",
+               "Laura",
+               "Melania"
+       )
+       
+       private val americanFamilyNames = listOf(
+               "Knox",
+               "Pickering",
+               "McHenry",
+               "Dexter",
+               "Drawborn",
+               "Eustis",
+               "Armstrong",
+               "Monroe",
+               "Crawford",
+               "Calhoun",
+               "Barbour",
+               "Porter",
+               "Eaton",
+               "Cass",
+               "Poinsett",
+               "Bell",
+               "Forrestal",
+               "Johnson",
+               "Marshall",
+               "Lovett",
+               "Wilson",
+               "McElroy",
+               "McNamara",
+               "Clifford",
+               "Richardson",
+               "Burndt",
+       )
+       
+       private fun randomAmericanName(isFemale: Boolean) = (if (isFemale) americanFemaleNames else americanMaleNames).random() + " " + americanFamilyNames.random()
+       
+       private val hispanicMaleNames = listOf(
+               "Aaron",
+               "Antonio",
+               "Augusto",
+               "Eliseo",
+               "Manuel",
+               "Jose",
+               "Juan",
+               "Miguel",
+               "Rafael",
+               "Raul",
+               "Adriano",
+               "Emilio",
+               "Francisco",
+               "Ignacio",
+               "Marco",
+               "Pablo",
+               "Octavio",
+               "Victor",
+               "Vito",
+               "Valentin"
+       )
+       
+       private val hispanicFemaleNames = listOf(
+               "Maria",
+               "Ana",
+               "Camila",
+               "Eva",
+               "Flora",
+               "Gloria",
+               "Julia",
+               "Marcelina",
+               "Rosalia",
+               "Victoria",
+               "Valentina",
+               "Cecilia",
+               "Francisca",
+               "Aurelia",
+               "Cristina",
+               "Magdalena",
+               "Margarita",
+               "Martina",
+               "Teresa"
+       )
+       
+       private val hispanicFamilyNames = listOf(
+               "Acorda",
+               "Aguirre",
+               "Alzaga",
+               "Arriaga",
+               "Arrieta",
+               "Berroya",
+               "Barahona",
+               "Carranza",
+               "Carriaga",
+               "Elcano",
+               "Elizaga",
+               "Endaya",
+               "Franco",
+               "Garalde",
+               "Ibarra",
+               "Juarez",
+               "Lazarte",
+               "Legarda",
+               "Madariaga",
+               "Medrano",
+               "Narvaez",
+               "Olano",
+               "Ricarte",
+               "Salazar",
+               "Uriarte",
+               "Varona",
+               "Vergar",
+       )
+       
+       private fun randomHispanicName(isFemale: Boolean) = (if (isFemale) hispanicFemaleNames else hispanicMaleNames).random() + " " + hispanicFamilyNames.random()
+       
+       fun randomName(flavor: AdmiralNameFlavor, isFemale: Boolean) = when (flavor) {
+               AdmiralNameFlavor.MECHYRDIA -> randomMechyrdianName(isFemale)
+               AdmiralNameFlavor.TYLA -> randomTylanName(isFemale)
+               AdmiralNameFlavor.CALIBOR -> randomCaliboreseName(isFemale)
+               AdmiralNameFlavor.OLYMPIA -> randomLatinName(isFemale)
+               AdmiralNameFlavor.DUTCH -> randomDutchName(isFemale)
+               AdmiralNameFlavor.NORTHERN_DIADOCHI -> randomNorthernDiadochiName(isFemale)
+               AdmiralNameFlavor.SOUTHERN_DIADOCHI -> randomSouthernDiadochiName(isFemale)
+               AdmiralNameFlavor.FULKREYKK -> randomThedishName(isFemale)
+               AdmiralNameFlavor.AMERICAN -> randomAmericanName(isFemale)
+               AdmiralNameFlavor.HISPANIC_AMERICAN -> randomHispanicName(isFemale)
+       }
+}
diff --git a/src/commonMain/kotlin/starshipfights/data/admiralty/ship_names.kt b/src/commonMain/kotlin/starshipfights/data/admiralty/ship_names.kt
new file mode 100644 (file)
index 0000000..d09ce98
--- /dev/null
@@ -0,0 +1,452 @@
+package starshipfights.data.admiralty
+
+import starshipfights.game.Faction
+import starshipfights.game.ShipWeightClass
+import kotlin.random.Random
+
+fun newShipName(faction: Faction, shipWeightClass: ShipWeightClass, existingNames: MutableSet<String>) = generateSequence {
+       nameShip(faction, shipWeightClass)
+}.take(20).dropWhile { it in existingNames }.firstOrNull()?.also { existingNames.add(it) }
+
+private val mechyrdianFrigateNames1 = listOf(
+       "Unconquerable",
+       "Indomitable",
+       "Invincible",
+       "Imperial",
+       "Regal",
+       "Royal",
+       "Imperious",
+       "Honorable",
+       "Defiant",
+       "Eternal",
+       "Infinite",
+       "Dominant",
+       "Divine",
+       "Righteous",
+       "Resplendent",
+       "Protective",
+       "Innocent",
+       "August",
+       "Loyal"
+)
+
+private val mechyrdianFrigateNames2 = listOf(
+       "Faith",
+       "Empire",
+       "Royalty",
+       "Regality",
+       "Honor",
+       "Defiance",
+       "Eternity",
+       "Dominator",
+       "Divinity",
+       "Right",
+       "Righteousness",
+       "Resplendency",
+       "Defender",
+       "Protector",
+       "Innocence",
+       "Victory",
+       "Duty",
+       "Loyalty"
+)
+
+private val mechyrdianCruiserNames1 = listOf(
+       "Defender of",
+       "Protector of",
+       "Shield of",
+       "Sword of",
+       "Champion of",
+       "Hero of",
+       "Salvation of",
+       "Savior of",
+       "Shining Light of",
+       "Righteous Flame of",
+       "Eternal Glory of",
+)
+
+private val mechyrdianCruiserNames2 = listOf(
+       "Mechyrd",
+       "Kaiserswelt",
+       "Tenno no Wakusei",
+       "Nova Roma",
+       "Mont Imperial",
+       "Tyla",
+       "Vensca",
+       "Kaltag",
+       "Languavarth Prime",
+       "Languavarth Secundum",
+       "Elcialot",
+       "Othon",
+       "Starport",
+       "Sacrilegum",
+       "New Constantinople",
+       "Fairhus",
+       "Praxagora",
+       "Karolina",
+       "Kozachnia",
+       "New New Amsterdam",
+       "Mundus Caesaris Divi",
+       "Saiwatta",
+       "Earth"
+)
+
+private val mechyrdianBattleshipNames = listOf(
+       "Kaiser Wilhelm I",
+       "Kaiser Wilhelm II",
+       "Empereur Napoléon I Bonaparte",
+       "Tsar Nikolaj II Romanov",
+       "Seliger Kaiser Karl I von Habsburg",
+       "Emperor Joshua A. Norton I",
+       "Emperor Meiji the Great",
+       "Emperor Jack G. Coleman",
+       "Emperor Trevor C. Neer",
+       "Emperor Connor F. Vance",
+       "Emperor Jean-Bédel Bokassa I",
+       "King Charles XII",
+       "King William I the Conqueror",
+       "King Alfred the Great",
+       "Gustavus Adolphus Magnus Rex",
+       "Queen Victoria",
+       "Kōnstantînos XI Dragásēs Palaiológos",
+       "Ioustinianós I ho Mégas",
+       "Kjarossa Liha Vilakauva",
+       "Kjarossa Tarkona Sovasra",
+       "Great King Kūruš",
+       "Queen Elizabeth II",
+       "Kjarossa Karelka Helasra",
+       "Imperātor Cæsar Dīvī Fīlius Augustus",
+       "Cæsar Nerva Trāiānus",
+       "King Kaleb of Axum"
+)
+
+private fun nameMechyrdianShip(weightClass: ShipWeightClass) = when (weightClass) {
+       ShipWeightClass.ESCORT -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
+       ShipWeightClass.DESTROYER -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
+       ShipWeightClass.CRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
+       ShipWeightClass.BATTLECRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
+       ShipWeightClass.BATTLESHIP -> mechyrdianBattleshipNames.random()
+       ShipWeightClass.BATTLE_BARGE -> mechyrdianBattleshipNames.random()
+       else -> error("Invalid Mechyrdian ship weight!")
+}
+
+private val masraDraetsenFrigateNames1 = listOf(
+       "Murderous",
+       "Hateful",
+       "Heinous",
+       "Pestilent",
+       "Corrupting",
+       "Homicidal",
+       "Deadly",
+       "Primordial",
+       "Painful",
+       "Agonizing",
+       "Spiteful",
+       "Odious",
+       "Miserating",
+       "Damned",
+       "Condemned",
+       "Hellish",
+       "Dark",
+       "Impious",
+       "Unfaithful",
+       "Abyssal",
+       "Furious",
+       "Vengeful",
+       "Spiritous"
+)
+
+private val masraDraetsenFrigateNames2 = listOf(
+       "Murder",
+       "Hate",
+       "Hatred",
+       "Pestilence",
+       "Corruption",
+       "Homicide",
+       "Massacre",
+       "Death",
+       "Agony",
+       "Pain",
+       "Suffering",
+       "Spite",
+       "Misery",
+       "Damnation",
+       "Hell",
+       "Darkness",
+       "Impiety",
+       "Faithlessness",
+       "Abyss",
+       "Fury",
+       "Vengeance",
+       "Spirit"
+)
+
+private val masraDraetsenCruiserNames1 = listOf(
+       "Despoiler of",
+       "Desecrator of",
+       "Desolator of",
+       "Destroyer of",
+       "Executioner of",
+       "Pillager of",
+       "Villain of",
+       "Great Devil of",
+       "Infidelity of",
+       "Incineration of",
+       "Immolation of",
+       "Crucifixion of",
+       "Unending Darkness of",
+)
+
+private val masraDraetsenCruiserNames2 = listOf(
+       // Diadochi space
+       "Eskhaton",
+       "Terminus",
+       "Tychiphage",
+       "Magaddu",
+       "Ghattusha",
+       "Three Suns",
+       "RB-5354",
+       "VT-3072",
+       "Siegsstern",
+       "Atzalstadt",
+       "Apex",
+       "Summit",
+       // Lyudareykk and Isarnareykk
+       "Vion Kann",
+       "Kasr Karul",
+       "Vladizapad",
+       // Chaebodes Star Empire
+       "Ultima Thule",
+       "Prenovez",
+       // Calibor and Vescar sectors
+       "Letum Angelorum",
+       "Pharsalus",
+       "Eutopia",
+       // Ferthlon and Olympia sectors
+       "Ferthlon Primus",
+       "Ferthlon Secundus",
+       "Nova Roma",
+       "Mont Imperial",
+)
+
+private const val masraDraetsenColossusName = "Boukephalas"
+
+private fun nameMasraDraetsenShip(weightClass: ShipWeightClass) = when (weightClass) {
+       ShipWeightClass.ESCORT -> "${masraDraetsenFrigateNames1.random()} ${masraDraetsenFrigateNames2.random()}"
+       ShipWeightClass.DESTROYER -> "${masraDraetsenFrigateNames1.random()} ${masraDraetsenFrigateNames2.random()}"
+       ShipWeightClass.CRUISER -> "${masraDraetsenCruiserNames1.random()} ${masraDraetsenCruiserNames2.random()}"
+       ShipWeightClass.GRAND_CRUISER -> "${masraDraetsenCruiserNames1.random()} ${masraDraetsenCruiserNames2.random()}"
+       ShipWeightClass.COLOSSUS -> masraDraetsenColossusName
+       else -> error("Invalid Masra Draetsen ship weight!")
+}
+
+private enum class LatinNounForm {
+       MAS_SG,
+       FEM_SG,
+       NEU_SG,
+       MAS_PL,
+       FEM_PL,
+       NEU_PL,
+}
+
+private data class LatinNoun(
+       val noun: String,
+       val form: LatinNounForm
+)
+
+private data class LatinAdjective(
+       val masculineSingular: String,
+       val feminineSingular: String,
+       val neuterSingular: String,
+       val masculinePlural: String,
+       val femininePlural: String,
+       val neuterPlural: String,
+) {
+       fun get(form: LatinNounForm) = when (form) {
+               LatinNounForm.MAS_SG -> masculineSingular
+               LatinNounForm.FEM_SG -> feminineSingular
+               LatinNounForm.NEU_SG -> neuterSingular
+               LatinNounForm.MAS_PL -> masculinePlural
+               LatinNounForm.FEM_PL -> femininePlural
+               LatinNounForm.NEU_PL -> neuterPlural
+       }
+}
+
+private infix fun LatinNoun.describedBy(adjective: LatinAdjective) = "$noun ${adjective.get(form)}"
+
+private fun felinaeFelicesEscortShipName() = "ES-" + (1000..9999).random().toString()
+
+private val felinaeFelicesLineShipNames1 = listOf(
+       LatinNoun("Aevum", LatinNounForm.NEU_SG),
+       LatinNoun("Aquila", LatinNounForm.FEM_SG),
+       LatinNoun("Argonauta", LatinNounForm.MAS_SG),
+       LatinNoun("Cattus", LatinNounForm.MAS_SG),
+       LatinNoun("Daemon", LatinNounForm.MAS_SG),
+       LatinNoun("Divitia", LatinNounForm.FEM_SG),
+       LatinNoun("Feles", LatinNounForm.FEM_SG),
+       LatinNoun("Imperium", LatinNounForm.NEU_SG),
+       LatinNoun("Ius", LatinNounForm.NEU_SG),
+       LatinNoun("Iustitia", LatinNounForm.FEM_SG),
+       LatinNoun("Leo", LatinNounForm.MAS_SG),
+       LatinNoun("Leopardus", LatinNounForm.MAS_SG),
+       LatinNoun("Lynx", LatinNounForm.FEM_SG),
+       LatinNoun("Panthera", LatinNounForm.FEM_SG),
+       LatinNoun("Salvator", LatinNounForm.MAS_SG),
+       LatinNoun("Scelus", LatinNounForm.NEU_SG),
+       LatinNoun("Tigris", LatinNounForm.MAS_SG),
+)
+
+private val felinaeFelicesLineShipNames2 = listOf(
+       LatinAdjective("Animosus", "Animosa", "Animosum", "Animosi", "Animosae", "Animosa"),
+       LatinAdjective("Ardens", "Ardens", "Ardens", "Ardentes", "Ardentes", "Ardentia"),
+       LatinAdjective("Audax", "Audax", "Audax", "Audaces", "Audaces", "Audacia"),
+       LatinAdjective("Astutus", "Astuta", "Astutum", "Astuti", "Astutae", "Astuta"),
+       LatinAdjective("Calidus", "Calida", "Calidum", "Calidi", "Calidae", "Calida"),
+       LatinAdjective("Ferox", "Ferox", "Ferox", "Feroces", "Feroces", "Ferocia"),
+       LatinAdjective("Fortis", "Fortis", "Forte", "Fortes", "Fortes", "Fortia"),
+       LatinAdjective("Fugax", "Fugax", "Fugax", "Fugaces", "Fugaces", "Fugacia"),
+       LatinAdjective("Indomitus", "Indomita", "Indomitum", "Indomiti", "Indomitae", "Indomita"),
+       LatinAdjective("Intrepidus", "Intrepida", "Intrepidum", "Intrepidi", "Intrepidae", "Intrepida"),
+       LatinAdjective("Pervicax", "Pervicax", "Pervicax", "Pervicaces", "Pervicaces", "Pervicacia"),
+       LatinAdjective("Sagax", "Sagax", "Sagax", "Sagaces", "Sagaces", "Sagacia"),
+       LatinAdjective("Superbus", "Superba", "Superbum", "Superbi", "Superbae", "Superba"),
+       LatinAdjective("Trux", "Trux", "Trux", "Truces", "Truces", "Trucia"),
+)
+
+private fun nameFelinaeFelicesShip(weightClass: ShipWeightClass) = when (weightClass) {
+       ShipWeightClass.FF_ESCORT -> felinaeFelicesEscortShipName()
+       ShipWeightClass.FF_DESTROYER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
+       ShipWeightClass.FF_CRUISER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
+       ShipWeightClass.FF_BATTLECRUISER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
+       ShipWeightClass.FF_BATTLESHIP -> if (Random.nextDouble() < 0.01) "Big Floppa" else (felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random())
+       else -> error("Invalid Felinae Felices ship weight!")
+}
+
+private val isarnareykkShipNames = listOf(
+       "Professional with Standards",
+       "Online Game Cheater",
+       "Actually Made of Antimatter",
+       "Chucklehead",
+       "Guns Strapped to an Engine",
+       "Unidentified Comet",
+       "Deep Space Encounter",
+       "The Goggles Do Nothing",
+       "Sensor Error",
+       "ERROR SHIP NAME NOT FOUND",
+       "0x426F6174",
+       "Börgenkub",
+       "Instant Death",
+       "Assume The Position",
+       "Negative Space Wedgie",
+       "Tea, Earl Grey, Hot",
+       "There's Coffee In That Nebula",
+       "SPEHSS MEHREENS",
+       "Inconspicuous Asteroid",
+       "Inflatable Toy Ship",
+       "HELP TRAPPED IN SHIP FACTORY",
+       "Illegal Meme Dealer",
+       "Reverse the Polarity!",
+       "Send Your Bank Info To Win 10,000 Marks",
+       "STOP CALLING ABOUT MY STARSHIP WARRANTY",
+       "Somebody Once Told Me...",
+       "Praethoris Khorr Gaming",
+)
+
+private fun nameIsarnareykskShip() = isarnareykkShipNames.random()
+
+private val vestigiumShipNames = listOf(
+       // NAMED AFTER SPACE SHUTTLES
+       "Enterprise", // OV-101
+       "Columbia", // OV-102
+       "Discovery", // OV-103
+       "Atlantis", // OV-104
+       "Endeavor", // OV-105
+       "Conqueror", // OV-106
+       "Homeland", // OV-107
+       "Augustus", // OV-108
+       "Avenger", // OV-109
+       "Protector", // OV-110
+       
+       // NAMED AFTER HISTORICAL SHIPS
+       "Yorktown",
+       "Lexington",
+       "Ranger",
+       "Hornet",
+       "Wasp",
+       "Antares",
+       "Belfast",
+       // NAMED AFTER PLACES
+       "Akron",
+       "Hudson",
+       "Cleveland",
+       "Baltimore",
+       "Bel Air",
+       "Cedar Rapids",
+       "McHenry",
+       "Rochester",
+       "Cuyahoga Valley",
+       "Catonsville",
+       "Ocean City",
+       "Philadelphia",
+       "Somerset",
+       "Pittsburgh",
+       
+       "Las Vegas",
+       "Reno",
+       "Boulder City",
+       "Goodsprings",
+       "Nipton",
+       "Primm",
+       "Nellis",
+       "Fortification Hill",
+       "McCarran",
+       "Fremont",
+       
+       // NAMED AFTER SPACE PROBES
+       "Voyager",
+       "Juno",
+       "Cassini",
+       "Hubble",
+       "Huygens",
+       "Pioneer",
+       
+       // NAMED AFTER PEOPLE
+       // Founding Fathers
+       "George Washington",
+       "Thomas Jefferson",
+       "John Adams",
+       "Alexander Hamilton",
+       "James Madison",
+       // US Presidents
+       "Andrew Jackson",
+       "Abraham Lincoln",
+       "Theodore Roosevelt",
+       "Calvin Coolidge",
+       "Dwight Eisenhower",
+       "Richard Nixon",
+       "Ronald Reagan",
+       "Donald Trump",
+       "Ron DeSantis",
+       "Gary Martison",
+       // IS Emperors
+       "Jack Coleman",
+       "Trevor Neer",
+       "Hadrey Trevison",
+       "Dio Audrey",
+       "Connor Vance",
+       // Vestigium Leaders
+       "Thomas Blackrock",
+       "Philip Mack",
+       "Ilya Korochenko"
+)
+
+private fun nameAmericanShip() = vestigiumShipNames.random()
+
+fun nameShip(faction: Faction, weightClass: ShipWeightClass): String = when (faction) {
+       Faction.MECHYRDIA -> nameMechyrdianShip(weightClass)
+       Faction.NDRC -> nameMechyrdianShip(weightClass)
+       Faction.MASRA_DRAETSEN -> nameMasraDraetsenShip(weightClass)
+       Faction.FELINAE_FELICES -> nameFelinaeFelicesShip(weightClass)
+       Faction.ISARNAREYKK -> nameIsarnareykskShip()
+       Faction.VESTIGIUM -> nameAmericanShip()
+}
index b2f036828c721ec313dad715329a542652753a1f..3b01eca14a8d4c7be61307c7d0366350b5095170 100644 (file)
@@ -241,7 +241,8 @@ fun generateOptimizationInitialState(hostFaction: Faction, guestFaction: Faction
                        faction = guestFaction,
                        rank = rank
                ),
-               battleInfo = battleInfo
+               battleInfo = battleInfo,
+               subplots = emptySet(),
        )
 }
 
index 9334a2a7113c4c3c066f66221e9988c8f97db840..523135171cd2a88e20c64504f86cdea2570453e9 100644 (file)
@@ -31,7 +31,12 @@ sealed class GameEvent {
        data class InvalidAction(val message: String) : GameEvent()
        
        @Serializable
-       data class GameEnd(val winner: GlobalSide?, val message: String) : GameEvent()
+       data class GameEnd(
+               val winner: GlobalSide?,
+               val message: String,
+               @Serializable(with = MapAsListSerializer::class)
+               val subplotOutcomes: Map<SubplotKey, SubplotOutcome> = emptyMap()
+       ) : GameEvent()
 }
 
 fun GameState.after(player: GlobalSide, packet: PlayerAction): GameEvent = when (packet) {
@@ -56,12 +61,21 @@ fun GameState.after(player: GlobalSide, packet: PlayerAction): GameEvent = when
                val loserName = admiralInfo(player).fullName
                val winnerName = admiralInfo(player.other).fullName
                
-               GameEvent.GameEnd(player.other, "$loserName never joined the battle, yielding victory to $winnerName!")
+               GameEvent.GameEnd(player.other, "$loserName never joined the battle, yielding victory to $winnerName!", emptyMap())
        }
        PlayerAction.Disconnect -> {
                val loserName = admiralInfo(player).fullName
                val winnerName = admiralInfo(player.other).fullName
                
-               GameEvent.GameEnd(player.other, "$loserName has disconnected from the battle, yielding victory to $winnerName!")
+               GameEvent.GameEnd(player.other, "$loserName has disconnected from the battle, yielding victory to $winnerName!", emptyMap())
        }
+}.let { event ->
+       if (event is GameEvent.StateChange) {
+               val subplotKeys = event.newState.subplots.map { it.key }
+               val finalState = subplotKeys.fold(event.newState) { newState, key ->
+                       val subplot = newState.subplots.single { it.key == key }
+                       subplot.onGameStateChanged(newState)
+               }
+               GameEvent.StateChange(finalState)
+       } else event
 }
index 75585308b6b85044fe5cb540ed157c505e0ecf97..231362bf4c834e0a12b395b152265b8886cf4cfa 100644 (file)
@@ -11,6 +11,8 @@ data class GameState(
        val guestInfo: InGameAdmiral,
        val battleInfo: BattleInfo,
        
+       val subplots: Set<Subplot>,
+       
        val phase: GamePhase = GamePhase.Deploy,
        val doneWithPhase: GlobalSide? = null,
        val calculatedInitiative: GlobalSide? = null,
@@ -21,7 +23,10 @@ data class GameState(
        val chatBox: List<ChatEntry> = emptyList(),
 ) {
        fun getShipInfo(id: Id<ShipInstance>) = destroyedShips[id]?.ship ?: ships.getValue(id).ship
+       fun getShipInfoOrNull(id: Id<ShipInstance>) = destroyedShips[id]?.ship ?: ships[id]?.ship
+       
        fun getShipOwner(id: Id<ShipInstance>) = destroyedShips[id]?.owner ?: ships.getValue(id).owner
+       fun getShipOwnerOrNull(id: Id<ShipInstance>) = destroyedShips[id]?.owner ?: ships[id]?.owner
 }
 
 val GameState.currentInitiative: GlobalSide?
@@ -47,6 +52,17 @@ private fun GameState.afterPhase(): GameState {
        var newInitiative: GameState.() -> InitiativePair = { InitiativePair(emptyMap()) }
        
        when (phase) {
+               GamePhase.Deploy -> {
+                       return subplots.map { it.key }.fold(this) { newState, key ->
+                               val subplot = newState.subplots.single { it.key == key }
+                               subplot.onAfterDeployShips(newState)
+                       }.copy(
+                               phase = phase.next(),
+                               ships = ships.mapValues { (_, ship) ->
+                                       ship.copy(isDoneCurrentPhase = false)
+                               },
+                       )
+               }
                is GamePhase.Power -> {
                        // Prepare for move phase
                        newInitiative = { calculateMovePhaseInitiative() }
@@ -162,12 +178,24 @@ fun GameState.checkVictory(): GameEvent.GameEnd? {
        val hostDefeated = ships.none { (_, it) -> it.owner == GlobalSide.HOST }
        val guestDefeated = ships.none { (_, it) -> it.owner == GlobalSide.GUEST }
        
+       val winner = if (hostDefeated && guestDefeated)
+               null
+       else if (hostDefeated)
+               GlobalSide.GUEST
+       else if (guestDefeated)
+               GlobalSide.HOST
+       else return null
+       
+       val subplotsOutcomes = subplots.associate { subplot ->
+               subplot.key to subplot.getFinalGameResult(this, winner)
+       }
+       
        return if (hostDefeated && guestDefeated)
-               GameEvent.GameEnd(null, "Stalemate: both sides have been completely destroyed!")
+               GameEvent.GameEnd(null, "Stalemate: both sides have been completely destroyed!", subplotsOutcomes)
        else if (hostDefeated)
-               GameEvent.GameEnd(GlobalSide.GUEST, victoryMessage(GlobalSide.GUEST))
+               GameEvent.GameEnd(GlobalSide.GUEST, victoryMessage(GlobalSide.GUEST), subplotsOutcomes)
        else if (guestDefeated)
-               GameEvent.GameEnd(GlobalSide.HOST, victoryMessage(GlobalSide.HOST))
+               GameEvent.GameEnd(GlobalSide.HOST, victoryMessage(GlobalSide.HOST), subplotsOutcomes)
        else
                null
 }
diff --git a/src/commonMain/kotlin/starshipfights/game/game_subplots.kt b/src/commonMain/kotlin/starshipfights/game/game_subplots.kt
new file mode 100644 (file)
index 0000000..8b41938
--- /dev/null
@@ -0,0 +1,296 @@
+package starshipfights.game
+
+import kotlinx.serialization.Serializable
+import starshipfights.data.Id
+
+@Serializable
+data class GameObjective(
+       val displayText: String,
+       val succeeded: Boolean?
+)
+
+fun GameState.objectives(forPlayer: GlobalSide): List<GameObjective> = listOf(
+       GameObjective("Destroy or rout the enemy fleet", null)
+) + subplots.filter { it.forPlayer == forPlayer }.mapNotNull { it.displayObjective(this) }
+
+@Serializable
+data class SubplotKey(
+       val type: SubplotType,
+       val player: GlobalSide,
+)
+
+val Subplot.key: SubplotKey
+       get() = SubplotKey(type, forPlayer)
+
+@Serializable
+sealed class Subplot {
+       abstract val type: SubplotType
+       abstract val displayName: String
+       abstract val forPlayer: GlobalSide
+       
+       override fun equals(other: Any?): Boolean {
+               return other is Subplot && other.key == key
+       }
+       
+       override fun hashCode(): Int {
+               return key.hashCode()
+       }
+       
+       abstract fun displayObjective(gameState: GameState): GameObjective?
+       
+       abstract fun onAfterDeployShips(gameState: GameState): GameState
+       abstract fun onGameStateChanged(gameState: GameState): GameState
+       abstract fun getFinalGameResult(gameState: GameState, winner: GlobalSide?): SubplotOutcome
+       
+       protected fun GameState.modifySubplotData(newSubplot: Subplot) = copy(subplots = (subplots - this@Subplot) + newSubplot)
+       
+       @Serializable
+       class ExtendedDuty(override val forPlayer: GlobalSide) : Subplot() {
+               override val type: SubplotType
+                       get() = SubplotType.EXTENDED_DUTY
+               
+               override val displayName: String
+                       get() = "Extended Duty"
+               
+               override fun displayObjective(gameState: GameState) = GameObjective("Win the battle with your fleet worn out from extended duty", null)
+               
+               private fun ShipInstance.preBattleDamage(): ShipInstance = when ((0..4).random()) {
+                       0 -> copy(
+                               hullAmount = (2..hullAmount).random(),
+                               troopsAmount = (2..troopsAmount).random(),
+                               modulesStatus = ShipModulesStatus(
+                                       modulesStatus.statuses.mapValues { (_, status) ->
+                                               if (status != ShipModuleStatus.ABSENT && (1..3).random() == 1)
+                                                       ShipModuleStatus.DESTROYED
+                                               else
+                                                       status
+                                       }
+                               )
+                       )
+                       1 -> copy(
+                               hullAmount = (2..hullAmount).random(),
+                               troopsAmount = (2..troopsAmount).random(),
+                       )
+                       2 -> copy(
+                               troopsAmount = (2..troopsAmount).random(),
+                       )
+                       else -> this
+               }
+               
+               override fun onAfterDeployShips(gameState: GameState) = gameState.copy(ships = gameState.ships.mapValues { (_, ship) ->
+                       if (ship.owner == forPlayer)
+                               ship.preBattleDamage()
+                       else ship
+               })
+               
+               override fun onGameStateChanged(gameState: GameState) = gameState
+               
+               override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = SubplotOutcome.fromBattleWinner(winner, forPlayer)
+       }
+       
+       @Serializable
+       class NoQuarter(override val forPlayer: GlobalSide) : Subplot() {
+               override val type: SubplotType
+                       get() = SubplotType.NO_QUARTER
+               
+               override val displayName: String
+                       get() = "Leave No Quarter!"
+               
+               override fun displayObjective(gameState: GameState): GameObjective {
+                       val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other }
+                       val enemyWrecks = gameState.destroyedShips.values.filter { it.owner == forPlayer.other }
+                       
+                       val totalEnemyShipPointCount = enemyShips.sumOf { it.ship.pointCost } + enemyWrecks.sumOf { it.ship.pointCost }
+                       val escapedShipPointCount = enemyWrecks.filter { it.isEscape }.sumOf { it.ship.pointCost }
+                       val destroyedShipPointCount = enemyWrecks.filter { !it.isEscape }.sumOf { it.ship.pointCost }
+                       
+                       val success = when {
+                               destroyedShipPointCount * 2 >= totalEnemyShipPointCount -> true
+                               escapedShipPointCount * 2 >= totalEnemyShipPointCount -> false
+                               else -> null
+                       }
+                       
+                       return GameObjective("Destroy at least half of the enemy fleet's point value - do not let them escape!", success)
+               }
+               
+               override fun onAfterDeployShips(gameState: GameState) = gameState
+               
+               override fun onGameStateChanged(gameState: GameState) = gameState
+               
+               override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?): SubplotOutcome {
+                       val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other }
+                       val enemyWrecks = gameState.destroyedShips.values.filter { it.owner == forPlayer.other }
+                       
+                       val totalEnemyShipPointCount = enemyShips.sumOf { it.ship.pointCost } + enemyWrecks.sumOf { it.ship.pointCost }
+                       val destroyedShipPointCount = enemyWrecks.filter { !it.isEscape }.sumOf { it.ship.pointCost }
+                       
+                       return if (destroyedShipPointCount * 2 >= totalEnemyShipPointCount)
+                               SubplotOutcome.WON
+                       else
+                               SubplotOutcome.LOST
+               }
+       }
+       
+       @Serializable
+       class Vendetta private constructor(override val forPlayer: GlobalSide, private val againstShip: Id<ShipInstance>?, private val outcome: SubplotOutcome) : Subplot() {
+               constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED)
+               constructor(forPlayer: GlobalSide, againstShip: Id<ShipInstance>) : this(forPlayer, againstShip, SubplotOutcome.UNDECIDED)
+               
+               override val type: SubplotType
+                       get() = SubplotType.VENDETTA
+               
+               override val displayName: String
+                       get() = "Vendetta!"
+               
+               override fun displayObjective(gameState: GameState): GameObjective? {
+                       val shipName = gameState.getShipInfoOrNull(againstShip ?: return null)?.fullName ?: return null
+                       return GameObjective("Destroy the $shipName", outcome.toSuccess)
+               }
+               
+               override fun onAfterDeployShips(gameState: GameState): GameState {
+                       if (gameState.ships[againstShip] != null) return gameState
+                       
+                       val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other }
+                       val highestEnemyShipTier = enemyShips.maxOf { it.ship.shipType.weightClass }
+                       val enemyShipsOfHighestTier = enemyShips.filter { it.ship.shipType.weightClass == highestEnemyShipTier }
+                       
+                       val vendettaShip = enemyShipsOfHighestTier.random().id
+                       return gameState.modifySubplotData(Vendetta(forPlayer, vendettaShip, SubplotOutcome.UNDECIDED))
+               }
+               
+               override fun onGameStateChanged(gameState: GameState): GameState {
+                       if (outcome != SubplotOutcome.UNDECIDED) return gameState
+                       
+                       val vendettaShipWreck = gameState.destroyedShips[againstShip ?: return gameState] ?: return gameState
+                       return if (vendettaShipWreck.isEscape)
+                               gameState.modifySubplotData(Vendetta(forPlayer, againstShip, SubplotOutcome.LOST))
+                       else
+                               gameState.modifySubplotData(Vendetta(forPlayer, againstShip, SubplotOutcome.WON))
+               }
+               
+               override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = if (outcome == SubplotOutcome.UNDECIDED)
+                       SubplotOutcome.LOST
+               else outcome
+       }
+       
+       @Serializable
+       class RecoverInformant private constructor(override val forPlayer: GlobalSide, private val onBoardShip: Id<ShipInstance>?, private val outcome: SubplotOutcome, private val mostRecentChatMessages: Moment?) : Subplot() {
+               constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED, null)
+               constructor(forPlayer: GlobalSide, onBoardShip: Id<ShipInstance>) : this(forPlayer, onBoardShip, SubplotOutcome.UNDECIDED, null)
+               
+               override val type: SubplotType
+                       get() = SubplotType.RECOVER_INFORMANT
+               
+               override val displayName: String
+                       get() = "Recover Informant"
+               
+               override fun displayObjective(gameState: GameState): GameObjective? {
+                       val shipName = gameState.getShipInfoOrNull(onBoardShip ?: return null)?.fullName ?: return null
+                       return GameObjective("Board the $shipName and recover your informant", outcome.toSuccess)
+               }
+               
+               override fun onAfterDeployShips(gameState: GameState): GameState {
+                       if (gameState.ships[onBoardShip] != null) return gameState
+                       
+                       val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other }
+                       val lowestEnemyShipTier = enemyShips.minOf { it.ship.shipType.weightClass }
+                       val enemyShipsNotOfLowestTier = enemyShips.filter { it.ship.shipType.weightClass != lowestEnemyShipTier }.ifEmpty { enemyShips }
+                       
+                       val informantShip = enemyShipsNotOfLowestTier.random().id
+                       return gameState.modifySubplotData(RecoverInformant(forPlayer, informantShip, SubplotOutcome.UNDECIDED, null))
+               }
+               
+               private fun GameState.getNewMessages(readTime: Moment?) = if (readTime == null)
+                       chatBox
+               else
+                       chatBox.filter { it.sentAt > readTime }
+               
+               override fun onGameStateChanged(gameState: GameState): GameState {
+                       if (outcome != SubplotOutcome.UNDECIDED) return gameState
+                       
+                       var readTime = mostRecentChatMessages
+                       for (message in gameState.getNewMessages(mostRecentChatMessages)) {
+                               when (message) {
+                                       is ChatEntry.ShipEscaped -> if (message.ship == onBoardShip)
+                                               return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.LOST, null))
+                                       is ChatEntry.ShipDestroyed -> if (message.ship == onBoardShip)
+                                               return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.LOST, null))
+                                       is ChatEntry.ShipBoarded -> if (message.ship == onBoardShip && (1..3).random() == 1)
+                                               return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.WON, null))
+                                       else -> {
+                                               // do nothing
+                                       }
+                               }
+                               readTime = if (readTime == null || readTime < message.sentAt) message.sentAt else readTime
+                       }
+                       
+                       return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.UNDECIDED, readTime))
+               }
+               
+               override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = if (outcome == SubplotOutcome.UNDECIDED)
+                       SubplotOutcome.LOST
+               else outcome
+       }
+}
+
+enum class SubplotType(val factory: (GlobalSide) -> Subplot) {
+       EXTENDED_DUTY(Subplot::ExtendedDuty),
+       NO_QUARTER(Subplot::NoQuarter),
+       VENDETTA(Subplot::Vendetta),
+       RECOVER_INFORMANT(Subplot::RecoverInformant),
+}
+
+fun generateSubplots(battleSize: BattleSize, forPlayer: GlobalSide): Set<Subplot> =
+       (1..battleSize.numSubplotsPerPlayer).map {
+               SubplotType.values().random().factory(forPlayer)
+       }.toSet()
+
+@Serializable
+enum class SubplotOutcome {
+       UNDECIDED, WON, LOST;
+       
+       val toSuccess: Boolean?
+               get() = when (this) {
+                       UNDECIDED -> null
+                       WON -> true
+                       LOST -> false
+               }
+       
+       companion object {
+               fun fromBattleWinner(winner: GlobalSide?, subplotForPlayer: GlobalSide) = when (winner) {
+                       subplotForPlayer -> WON
+                       subplotForPlayer.other -> LOST
+                       else -> UNDECIDED
+               }
+       }
+}
+
+fun victoryTitle(player: GlobalSide, winner: GlobalSide?, subplotOutcomes: Map<SubplotKey, SubplotOutcome>): String {
+       val myOutcomes = subplotOutcomes.filterKeys { it.player == player }
+       
+       return when (winner) {
+               player -> {
+                       val isGlorious = myOutcomes.all { (_, outcome) -> outcome == SubplotOutcome.WON }
+                       val isPyrrhic = myOutcomes.size >= 2 && myOutcomes.any { (_, outcome) -> outcome != SubplotOutcome.WON }
+                       
+                       if (isGlorious)
+                               "Glorious Victory"
+                       else if (isPyrrhic)
+                               "Pyrrhic Victory"
+                       else
+                               "Victory"
+               }
+               player.other -> {
+                       val isHeroic = myOutcomes.all { (_, outcome) -> outcome == SubplotOutcome.WON }
+                       val isHumiliating = myOutcomes.size >= 2 && myOutcomes.any { (_, outcome) -> outcome != SubplotOutcome.WON }
+                       
+                       if (isHeroic)
+                               "Heroic Defeat"
+                       else if (isHumiliating)
+                               "Humiliating Defeat"
+                       else
+                               "Defeat"
+               }
+               else -> "Stalemate"
+       }
+}
index 88ea5b8e70416b0f63430b17b9bed27d32e4cc3e..bbfe00af29e57c6ba0637ddf88edd07902948a91 100644 (file)
@@ -14,6 +14,18 @@ enum class BattleSize(val numPoints: Int, val maxWeightClass: ShipWeightClass, v
        CRUCIBLE_OF_HISTORY(3000, ShipWeightClass.COLOSSUS, "Crucible of History");
 }
 
+val BattleSize.numSubplotsPerPlayer: Int
+       get() = when (this) {
+               BattleSize.SKIRMISH -> 0
+               BattleSize.RAID -> 0
+               BattleSize.FIREFIGHT -> 0
+               BattleSize.BATTLE -> (0..1).random()
+               BattleSize.GRAND_CLASH -> 1
+               BattleSize.APOCALYPSE -> 1
+               BattleSize.LEGENDARY_STRUGGLE -> 1
+               BattleSize.CRUCIBLE_OF_HISTORY -> (1..2).random()
+       }
+
 enum class BattleBackground(val displayName: String, val color: String) {
        BLUE_BROWN("Milky Way", "#335577"),
        BLUE_MAGENTA("Arcane Anomaly", "#553377"),
diff --git a/src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt b/src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt
new file mode 100644 (file)
index 0000000..6d6cbd1
--- /dev/null
@@ -0,0 +1,3 @@
+package starshipfights.game
+
+
index 07807e9c1e64a543ceeb01d7a5e640190eb25186..6618d987547a9133335e6110bd6dc4ede01811fe 100644 (file)
@@ -1,6 +1,12 @@
 package starshipfights.game
 
 import kotlinx.html.*
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.builtins.PairSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.json.Json
 import kotlin.math.abs
 import kotlin.math.exp
@@ -15,6 +21,21 @@ val jsonSerializer = Json {
        useAlternativeNames = false
 }
 
+class MapAsListSerializer<K, V>(keySerializer: KSerializer<K>, valueSerializer: KSerializer<V>) : KSerializer<Map<K, V>> {
+       private val inner = ListSerializer(PairSerializer(keySerializer, valueSerializer))
+       
+       override val descriptor: SerialDescriptor
+               get() = inner.descriptor
+       
+       override fun serialize(encoder: Encoder, value: Map<K, V>) {
+               inner.serialize(encoder, value.toList())
+       }
+       
+       override fun deserialize(decoder: Decoder): Map<K, V> {
+               return inner.deserialize(decoder).toMap()
+       }
+}
+
 const val EPSILON = 0.00_001
 
 fun <T : Enum<T>> T.toUrlSlug() = name.replace('_', '-').lowercase()
index 6448316d0b71908bbb7302055aad9118abc7da6a..2a43721ff24603aeb0bf7688cc29db3abf0738af 100644 (file)
@@ -114,8 +114,8 @@ suspend fun GameRenderInteraction.execute(scope: CoroutineScope) {
        }
 }
 
-private suspend fun GameNetworkInteraction.execute(token: String): Pair<LocalSide?, String> {
-       val gameEnd = CompletableDeferred<Pair<LocalSide?, String>>()
+private suspend fun GameNetworkInteraction.execute(token: String): GameEvent.GameEnd {
+       val gameEnd = CompletableDeferred<GameEvent.GameEnd>()
        
        try {
                httpClient.webSocket("$rootPathWs/game/$token") {
@@ -124,7 +124,7 @@ private suspend fun GameNetworkInteraction.execute(token: String): Pair<LocalSid
                        }.display()
                        
                        if (!opponentJoined)
-                               Popup.GameOver(LocalSide.GREEN, "Unfortunately, your opponent never entered the battle.", gameState.value).display()
+                               Popup.GameOver(mySide, "Unfortunately, your opponent never entered the battle.", emptyMap(), gameState.value).display()
                        
                        val sendActionsJob = launch {
                                for (action in playerActions)
@@ -142,18 +142,18 @@ private suspend fun GameNetworkInteraction.execute(token: String): Pair<LocalSid
                                                errorMessages.send(event.message)
                                        }
                                        is GameEvent.GameEnd -> {
-                                               gameEnd.complete(event.winner?.relativeTo(mySide) to event.message)
+                                               gameEnd.complete(event)
                                                closeAndReturn { return@webSocket sendActionsJob.cancel() }
                                        }
                                }
                        }
                }
        } catch (ex: WebSocketException) {
-               gameEnd.complete(null to "Server closed connection abruptly")
+               gameEnd.complete(GameEvent.GameEnd(null, "Server closed connection abruptly", emptyMap()))
        }
        
        if (gameEnd.isActive)
-               gameEnd.complete(null to "Connection closed")
+               gameEnd.complete(GameEvent.GameEnd(null, "Connection closed", emptyMap()))
        
        return gameEnd.await()
 }
@@ -195,10 +195,10 @@ suspend fun gameMain(side: GlobalSide, token: String, state: GameState) {
                val connectionJob = async { gameConnection.execute(token) }
                val renderingJob = launch { gameRendering.execute(this@coroutineScope) }
                
-               val (finalWinner, finalMessage) = connectionJob.await()
+               val (finalWinner, finalMessage, finalSubplots) = connectionJob.await()
                renderingJob.cancel()
                
                interruptExit = false
-               Popup.GameOver(finalWinner, finalMessage, gameState.value).display()
+               Popup.GameOver(finalWinner, finalMessage, finalSubplots, gameState.value).display()
        }
 }
index a4bacf83f376804e67e9299c551e570ad9986cb0..b6f178c8e1325bdb7864ff0515b624ba292b2716 100644 (file)
@@ -51,7 +51,7 @@ class GameSession(gameState: GameState) {
        }
 }
 
-private suspend fun GameNetworkInteraction.execute(): Pair<LocalSide?, String> {
+private suspend fun GameNetworkInteraction.execute(): GameEvent.GameEnd {
        val gameSession = GameSession(gameState.value)
        
        val aiSide = mySide.other
@@ -113,7 +113,7 @@ private suspend fun GameNetworkInteraction.execute(): Pair<LocalSide?, String> {
                aiHandlingJob.cancel()
                playerHandlingJob.cancel()
                
-               gameEnd.winner?.relativeTo(mySide) to gameEnd.message
+               gameEnd
        }
 }
 
@@ -135,10 +135,10 @@ suspend fun trainingMain(state: GameState) {
                val connectionJob = async { gameConnection.execute() }
                val renderingJob = launch { gameRendering.execute(this@coroutineScope) }
                
-               val (finalWinner, finalMessage) = connectionJob.await()
+               val (finalWinner, finalMessage, finalSubplots) = connectionJob.await()
                renderingJob.cancel()
                
                interruptExit = false
-               Popup.GameOver(finalWinner, finalMessage, gameState.value).display()
+               Popup.GameOver(finalWinner, finalMessage, finalSubplots, gameState.value).display()
        }
 }
index 8b1be0a4f93c8c570e28d2a3e8ef080b24b0de18..d18225e56ef00df47f8366d1cd51b5806bc5465c 100644 (file)
@@ -31,6 +31,8 @@ object GameUI {
        private lateinit var topMiddleInfo: HTMLDivElement
        private lateinit var topRightBar: HTMLDivElement
        
+       private lateinit var objectives: HTMLDivElement
+       
        private lateinit var errorMessages: HTMLParagraphElement
        private lateinit var helpMessages: HTMLParagraphElement
        
@@ -79,6 +81,10 @@ object GameUI {
                                id = "top-right-bar"
                        }
                        
+                       div {
+                               id = "objectives"
+                       }
+                       
                        p {
                                id = "error-messages"
                        }
@@ -114,6 +120,8 @@ object GameUI {
                topMiddleInfo = document.getElementById("top-middle-info").unsafeCast<HTMLDivElement>()
                topRightBar = document.getElementById("top-right-bar").unsafeCast<HTMLDivElement>()
                
+               objectives = document.getElementById("objectives").unsafeCast<HTMLDivElement>()
+               
                errorMessages = document.getElementById("error-messages").unsafeCast<HTMLParagraphElement>()
                helpMessages = document.getElementById("help-messages").unsafeCast<HTMLParagraphElement>()
                
@@ -394,6 +402,20 @@ object GameUI {
                        }
                }.lastOrNull()?.scrollIntoView()
                
+               objectives.clear()
+               objectives.append {
+                       for (objective in state.objectives(mySide)) {
+                               val classes = when (objective.succeeded) {
+                                       true -> "item succeeded"
+                                       false -> "item failed"
+                                       else -> "item"
+                               }
+                               div(classes = classes) {
+                                       +objective.displayText
+                               }
+                       }
+               }
+               
                val abilities = state.getPossibleAbilities(mySide)
                
                topMiddleInfo.clear()
index 70b58ea6f2bbc80f6eb8e0af2a3daf36493429b2..9ed0a7c8dcc4823438a5daa6dbad86fbef894d49 100644 (file)
@@ -497,17 +497,13 @@ sealed class Popup<out T> {
                }
        }
        
-       class GameOver(private val winner: LocalSide?, private val outcome: String, private val finalState: GameState) : Popup<Nothing>() {
+       class GameOver(private val winner: GlobalSide?, private val outcome: String, private val subplotStatuses: Map<SubplotKey, SubplotOutcome>, private val finalState: GameState) : Popup<Nothing>() {
                override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) {
                        p {
                                style = "text-align:center"
                                
                                strong(classes = "heading") {
-                                       +when (winner) {
-                                               LocalSide.GREEN -> "Victory"
-                                               LocalSide.RED -> "Defeat"
-                                               null -> "Stalemate"
-                                       }
+                                       +victoryTitle(mySide, winner, subplotStatuses)
                                }
                        }
                        p {
index 8ffb9ea9b9f6570b6a13bdea4d7c641718a628f8..4bde6524dd60907320c455cbc0544c56ae29dfe2 100644 (file)
@@ -219,15 +219,6 @@ hr + hr {
        display: none;
 }
 
-#bottom-center-bar {
-       position: fixed;
-       bottom: 22.5vh;
-       left: 50vw;
-       width: 25vw;
-       height: 5vh;
-       transform: translate(-50%, 0);
-}
-
 input[type=text] {
        border: none;
        border-bottom: 2px solid transparent;
@@ -375,3 +366,38 @@ button:disabled > img {
        color: #d22;
        font-weight: bold;
 }
+
+#objectives {
+       position: fixed;
+       top: 2.5vh;
+       left: 2.5vw;
+       width: 25vw;
+       height: 75vh;
+       font-size: 1.5em;
+}
+
+#objectives .item {
+       background-color: rgba(0, 0, 0, 0.6);
+       color: #cccccc;
+       padding: 1.2em 0.4em 1.2em 2em;
+}
+
+#objectives .item.failed {
+       color: #ff5555;
+}
+
+#objectives .item.failed::before {
+       content: '✘';
+       position: absolute;
+       left: 0.5em;
+}
+
+#objectives .item.succeeded {
+       color: #55ff55;
+}
+
+#objectives .item.succeeded::before {
+       content: '✔';
+       position: absolute;
+       left: 0.5em;
+}
diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt
deleted file mode 100644 (file)
index af93345..0000000
+++ /dev/null
@@ -1,861 +0,0 @@
-package starshipfights.data.admiralty
-
-import kotlin.random.Random
-
-enum class AdmiralNameFlavor {
-       MECHYRDIA, TYLA, CALIBOR, OLYMPIA, // Mechyrdia-aligned
-       DUTCH, // NdRC-aliged
-       NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI, // Masra Draetsen-aligned
-       FULKREYKK, // Isarnareykk-aligned
-       AMERICAN, HISPANIC_AMERICAN; // Vestigium-aligned
-       
-       val displayName: String
-               get() = when (this) {
-                       MECHYRDIA -> "Mechyrdian"
-                       TYLA -> "Tylan"
-                       CALIBOR -> "Caliborese"
-                       OLYMPIA -> "Olympian"
-                       DUTCH -> "Dutch"
-                       NORTHERN_DIADOCHI -> "Northern Diadochi"
-                       SOUTHERN_DIADOCHI -> "Southern Diadochi"
-                       FULKREYKK -> "Thedish"
-                       AMERICAN -> "American"
-                       HISPANIC_AMERICAN -> "Hispanic-American"
-               }
-}
-
-object AdmiralNames {
-       // PERSONAL NAME to PATRONYMIC
-       private val mechyrdianMaleNames: List<Pair<String, String>> = listOf(
-               "Marc" to "Marcówič",
-               "Anton" to "Antonówič",
-               "Bjarnarð" to "Bjarnarðówič",
-               "Carl" to "Carlówič",
-               "Þjutarix" to "Þjutarigówič",
-               "Friðurix" to "Friðurigówič",
-               "Iwan" to "Iwanówič",
-               "Wladimer" to "Wladimerówič",
-               "Giulius" to "Giuliówič",
-               "Nicólei" to "Nicóleiówič",
-               "Þjódor" to "Þjóderówič",
-               "Sigismund" to "Sigismundówič",
-               "Stefan" to "Stefanówič",
-               "Wilhelm" to "Wilhelmówič",
-               "Giórgj" to "Giórgiówič"
-       )
-       
-       // PERSONAL NAME to MATRONYMIC
-       private val mechyrdianFemaleNames: List<Pair<String, String>> = listOf(
-               "Octavia" to "Octaviówca",
-               "Annica" to "Annicówca",
-               "Astrið" to "Astriðówca",
-               "Caþarin" to "Caþarinówca",
-               "Signi" to "Signówca",
-               "Erica" to "Ericówca",
-               "Fréja" to "Fréjówca",
-               "Hilda" to "Hildówca",
-               "Žanna" to "Žannówca",
-               "Xenia" to "Xeniówca",
-               "Carina" to "Carinówca",
-               "Giadwiga" to "Giadwigówca",
-               "Ženia" to "Ženiówca"
-       )
-       
-       private val mechyrdianFamilyNames: List<Pair<String, String>> = listOf(
-               "Alexandrów",
-               "Antonów",
-               "Pogdanów",
-               "Hrusčjów",
-               "Caísarów",
-               "Carolów",
-               "Sócolów",
-               "Romanów",
-               "Nemeciów",
-               "Pjótrów",
-               "Brutów",
-               "Augustów",
-               "Calašniców",
-               "Anželów",
-               "Sigmarów",
-               "Dróganów",
-               "Coroljów",
-               "Wlasów"
-       ).map { it to "${it}a" }
-       
-       private fun randomMechyrdianName(isFemale: Boolean) = if (isFemale)
-               mechyrdianFemaleNames.random().first + " " + mechyrdianFemaleNames.random().second + " " + mechyrdianFamilyNames.random().second
-       else
-               mechyrdianMaleNames.random().first + " " + mechyrdianMaleNames.random().second + " " + mechyrdianFamilyNames.random().first
-       
-       private val tylanMaleNames = listOf(
-               "Althanar" to "Althanas",
-               "Aurans" to "Aurantes",
-               "Bochra" to "Bochranes",
-               "Chshaejar" to "Chshaejas",
-               "Hjofvachi" to "Hjovachines",
-               "Koldimar" to "Koldimas",
-               "Kor" to "Kores",
-               "Ljomas" to "Ljomates",
-               "Shajel" to "Shajel",
-               "Shokar" to "Shokas",
-               "Tolavajel" to "Tolavajel",
-               "Voskar" to "Voskas",
-       )
-       
-       private val tylanFemaleNames = listOf(
-               "Althe" to "Althenes",
-               "Anaseil" to "Anaseil",
-               "Asetbur" to "Asetbus",
-               "Atautha" to "Atauthas",
-               "Aurantia" to "Aurantias",
-               "Ilasheva" to "Ilashevas",
-               "Kalora" to "Kaloras",
-               "Kotolva" to "Kotolvas",
-               "Psekna" to "Pseknas",
-               "Shenera" to "Sheneras",
-               "Reoka" to "Reokas",
-               "Velga" to "Velgas",
-       )
-       
-       private val tylanFamilyNames = listOf(
-               "Kalevkar" to "Kalevka",
-               "Merku" to "Merkussa",
-               "Telet" to "Telet",
-               "Eutokar" to "Eutoka",
-               "Vsocha" to "Vsochessa",
-               "Vilar" to "Vilakauva",
-               "Nikasrar" to "Nika",
-               "Vlegamakar" to "Vlegamaka",
-               "Vtokassar" to "Vtoka",
-               "Theiar" to "Theia",
-               "Aretar" to "Areta",
-               "Derkas" to "Derkata",
-               "Vinsennas" to "Vinsenatta",
-               "Kleio" to "Kleona"
-       )
-       
-       // Tylans use matronymics for both sons and daughters
-       private fun randomTylanName(isFemale: Boolean) = if (isFemale)
-               tylanFemaleNames.random().first + " " + tylanFemaleNames.random().second + "-Nahra " + tylanFamilyNames.random().second
-       else
-               tylanMaleNames.random().first + " " + tylanFemaleNames.random().second + "-Nensar " + tylanFamilyNames.random().first
-       
-       private val caliboreseNames = listOf(
-               "Jathee",
-               "Muly",
-               "Simoh",
-               "Laka",
-               "Foryn",
-               "Duxio",
-               "Xirio",
-               "Surmy",
-               "Datarme",
-               "Cloren",
-               "Tared",
-               "Quiliot",
-               "Attiol",
-               "Quarree",
-               "Guil",
-               "Miro",
-               "Yryys",
-               "Zarx",
-               "Karm",
-               "Mreek",
-               "Dulyy",
-               "Quorqui",
-               "Dreminor",
-               "Samitu",
-               "Lurmak",
-               "Quashi",
-               "Barsyn",
-               "Rymyo",
-               "Soli",
-               "Ickart",
-               "Woom",
-               "Qurquy",
-               "Ymiro",
-               "Rosiliq",
-               "Xant",
-               "Xateen",
-               "Mssly",
-               "Vixie",
-               "Quelynn",
-               "Plly",
-               "Tessy",
-               "Veekah",
-               "Quett",
-               "Xezeez",
-               "Xyph",
-               "Jixi",
-               "Jeekie",
-               "Meelen",
-               "Rasah",
-               "Reteeshy",
-               "Xinchie",
-               "Zae",
-               "Ziggy",
-               "Wurikah",
-               "Loppie",
-               "Tymma",
-               "Reely",
-               "Yjutee",
-               "Len",
-               "Vixirat",
-               "Xumie",
-               "Xilly",
-               "Liwwy",
-               "Gancee",
-               "Pamah",
-               "Zeryll",
-               "Luteet",
-               "Qusseet",
-               "Alixika",
-               "Sepirah",
-               "Luttrah",
-               "Aramynn",
-               "Laxerynn",
-               "Murylyt",
-               "Quarapyt",
-               "Tormiray",
-               "Daromynn",
-               "Zuleerynn",
-               "Quarimat",
-               "Dormaquazi",
-               "Tullequazi",
-               "Aleeray",
-               "Eppiquit",
-               "Wittirynn",
-               "Semiokolipan",
-               "Sosopurr",
-               "Quamixit",
-               "Croffet",
-               "Xaalit",
-               "Xemiolyt"
-       )
-       
-       private val caliboreseVowels = "aeiouy".toSet()
-       private fun randomCaliboreseName(isFemale: Boolean) = caliboreseNames.filter {
-               it.length < 8 && (isFemale == (it.last() in caliboreseVowels))
-       }.random() + " " + caliboreseNames.filter { it.length > 7 }.random()
-       
-       private val latinMaleCommonPraenomina = listOf(
-               "Gaius",
-               "Lucius",
-               "Marcus",
-       )
-       
-       private val latinMaleUncommonPraenomina = listOf(
-               "Publius",
-               "Quintus",
-               "Titus",
-               "Gnaeus"
-       )
-       
-       private val latinMaleRarePraenomina = listOf(
-               "Aulus",
-               "Spurius",
-               "Tiberius",
-               "Servius",
-               "Hostus"
-       )
-       
-       private val latinFemaleCommonPraenomina = listOf(
-               "Gaia",
-               "Lucia",
-               "Marcia",
-       )
-       
-       private val latinFemaleUncommonPraenomina = listOf(
-               "Prima",
-               "Secunda",
-               "Tertia",
-               "Quarta",
-               "Quinta",
-               "Sexta",
-               "Septima",
-               "Octavia",
-               "Nona",
-               "Decima"
-       )
-       
-       private val latinFemaleRarePraenomina = listOf(
-               "Caesula",
-               "Titia",
-               "Tiberia",
-               "Tanaquil"
-       )
-       
-       private val latinNominaGentilica = listOf(
-               "Aelius" to "Aelia",
-               "Aternius" to "Aternia",
-               "Caecilius" to "Caecilia",
-               "Cassius" to "Cassia",
-               "Claudius" to "Claudia",
-               "Cornelius" to "Cornelia",
-               "Calpurnius" to "Calpurnia",
-               "Fabius" to "Fabia",
-               "Flavius" to "Flavia",
-               "Fulvius" to "Fulvia",
-               "Haterius" to "Hateria",
-               "Hostilius" to "Hostilia",
-               "Iulius" to "Iulia",
-               "Iunius" to "Iunia",
-               "Iuventius" to "Iuventia",
-               "Lavinius" to "Lavinia",
-               "Licinius" to "Licinia",
-               "Marius" to "Maria",
-               "Octavius" to "Octavia",
-               "Pompeius" to "Pompeia",
-               "Porcius" to "Porcia",
-               "Salvius" to "Salvia",
-               "Sempronius" to "Sempronia",
-               "Spurius" to "Spuria",
-               "Terentius" to "Terentia",
-               "Tullius" to "Tullia",
-               "Ulpius" to "Ulpia",
-               "Valerius" to "Valeria"
-       )
-       
-       private val latinCognomina = listOf(
-               "Agricola" to "Agricola",
-               "Agrippa" to "Agrippina",
-               "Aquilinus" to "Aquilina",
-               "Balbus" to "Balba",
-               "Bibulus" to "Bibula",
-               "Bucco" to "Bucco",
-               "Caecus" to "Caeca",
-               "Calidus" to "Calida",
-               "Catilina" to "Catilina",
-               "Catulus" to "Catula",
-               "Crassus" to "Crassa",
-               "Crispus" to "Crispa",
-               "Drusus" to "Drusilla",
-               "Flaccus" to "Flacca",
-               "Gracchus" to "Graccha",
-               "Laevinus" to "Laevina",
-               "Lanius" to "Lania",
-               "Lepidus" to "Lepida",
-               "Lucullus" to "Luculla",
-               "Marcellus" to "Marcella",
-               "Metellus" to "Metella",
-               "Nasica" to "Nasica",
-               "Nerva" to "Nerva",
-               "Paullus" to "Paulla",
-               "Piso" to "Piso",
-               "Priscus" to "Prisca",
-               "Publicola" to "Publicola",
-               "Pulcher" to "Pulchra",
-               "Regulus" to "Regula",
-               "Rufus" to "Rufa",
-               "Scaevola" to "Scaevola",
-               "Severus" to "Severa",
-               "Structus" to "Structa",
-               "Taurus" to "Taura",
-               "Varro" to "Varro",
-               "Vitulus" to "Vitula"
-       )
-       
-       private fun randomLatinPraenomen(isFemale: Boolean) = when {
-               Random.nextBoolean() -> if (isFemale) latinFemaleCommonPraenomina else latinMaleCommonPraenomina
-               Random.nextInt(3) > 0 -> if (isFemale) latinFemaleUncommonPraenomina else latinMaleUncommonPraenomina
-               else -> if (isFemale) latinFemaleRarePraenomina else latinMaleRarePraenomina
-       }.random()
-       
-       private fun randomLatinName(isFemale: Boolean) = randomLatinPraenomen(isFemale) + " " + latinNominaGentilica.random().let { (m, f) -> if (isFemale) f else m } + " " + latinCognomina.random().let { (m, f) -> if (isFemale) f else m }
-       
-       private val dutchMaleNames = listOf(
-               "Aalderik",
-               "Andreas",
-               "Boudewijn",
-               "Bruno",
-               "Christiaan",
-               "Cornelius",
-               "Darnath",
-               "Dirk",
-               "Eren",
-               "Erwin",
-               "Frederik",
-               "Gerlach",
-               "Helbrant",
-               "Helbrecht",
-               "Hendrik",
-               "Jakob",
-               "Jochem",
-               "Joris",
-               "Koenraad",
-               "Koorland",
-               "Leopold",
-               "Lodewijk",
-               "Maarten",
-               "Michel",
-               "Niels",
-               "Pieter",
-               "Renaat",
-               "Rogal",
-               "Ruben",
-               "Sebastiaan",
-               "Sigismund",
-               "Sjaak",
-               "Tobias",
-               "Valentijn",
-               "Wiebrand",
-       )
-       
-       private val dutchFemaleNames = listOf(
-               "Adelwijn",
-               "Amberlij",
-               "Annika",
-               "Arete",
-               "Eva",
-               "Gerda",
-               "Helga",
-               "Ida",
-               "Irene",
-               "Jacqueline",
-               "Josefien",
-               "Juliana",
-               "Katharijne",
-               "Lore",
-               "Margriet",
-               "Maximilia",
-               "Meike",
-               "Nora",
-               "Rebeka",
-               "Sara",
-               "Vera",
-               "Wilhelmina",
-       )
-       
-       private val dutchMerchantHouses = listOf(
-               "Venetho",
-               "Luibeck",
-               "Birka",
-               "Heiðabýr",
-               "Rostok",
-               "Guistrov",
-               "Schverin",
-               "Koeln",
-               "Bruigge",
-               "Reval",
-               "Elbing",
-               "Dorpat",
-               "Stralsund",
-               "Mijdeborg",
-               "Breslaw",
-               "Dortmund",
-               "Antwerp",
-               "Falsterbo",
-               "Zwolle",
-               "Buchtehud",
-               "Bremen",
-               "Zutphen",
-               "Kampen",
-               "Grunn",
-               "Deventer",
-               "Wismer",
-               "Luinenburg",
-               
-               "Jager",
-               "Jastobaal",
-               "Varonius",
-               "Kupferberg",
-               "Dijn",
-               "Umboldt",
-               "Phalomor",
-               "Drijk",
-               "d'Wain",
-               "du Languille",
-               "Horstein",
-               "Jerulas",
-               "Kendar",
-               "Castellan",
-               "d'Aniasie",
-               "Gerrit",
-               "Hoed",
-               "lo Pan",
-               "Marchandrij",
-               "d'Aquairre",
-               "Terozzante",
-               "d'Argovon",
-               "de Monde",
-               "Paillender",
-               "Holstijn",
-               "d'Imperia",
-               "Borodin",
-               "Agranozza",
-               "d'Ortise",
-               "Ijzerhoorn",
-               "Dremel",
-               "Hinckel",
-               "Vuigens",
-               "Drazen",
-               "Marburg",
-               "Xardt",
-               "Lijze",
-               "Gerlach",
-               "Doorn",
-               "d'Arquebus",
-               "Alderic",
-               "Vogen"
-       )
-       
-       private fun randomDutchName(isFemale: Boolean) = (if (isFemale) dutchFemaleNames else dutchMaleNames).random() + " van " + dutchMerchantHouses.random()
-       
-       private val diadochiMaleNames = listOf(
-               "Oqatai",
-               "Amogus",
-               "Nerokhan",
-               "Choghor",
-               "Aghonei",
-               "Martaq",
-               "Qaran",
-               "Khargh",
-               "Qolkhu",
-               "Ghauran",
-               "Woriv",
-               "Vorcha",
-               "Chagatai",
-               "Neghvar",
-               "Qitinga",
-               "Jimpaq",
-               "Bivat",
-               "Durash",
-               "Elifas",
-               "Ogus",
-               "Yuli",
-               "Saret",
-               "Mher",
-               "Tyver",
-               "Ghraq",
-               "Niran",
-               "Galik"
-       )
-       
-       private val diadochiFemaleNames = listOf(
-               "Lursha",
-               "Jamoqena",
-               "Lokoria",
-               "Iekuna",
-               "Shara",
-               "Etugen",
-               "Maral",
-               "Temuln",
-               "Akhensari",
-               "Khadagan",
-               "Gherelma",
-               "Shechen",
-               "Althani",
-               "Tzyrina",
-               "Daghasi",
-               "Kloya",
-       )
-       
-       private val northernDiadochiEpithetParts = listOf(
-               "Skull",
-               "Blood",
-               "Death",
-               "Claw",
-               "Doom",
-               "Dread",
-               "Soul",
-               "Spirit",
-               "Hell",
-               "Dread",
-               "Bale",
-               "Fire",
-               "Fist",
-               "Bear",
-               "Pyre",
-               "Dark",
-               "Vile",
-               "Heart",
-               "Murder",
-               "Gore",
-               "Daemon",
-               "Talon",
-       )
-       
-       private fun randomNorthernDiadochiName(isFemale: Boolean) = (if (isFemale) diadochiFemaleNames else diadochiMaleNames).random() + " " + northernDiadochiEpithetParts.random() + northernDiadochiEpithetParts.random().lowercase()
-       
-       private val southernDiadochiClans = listOf(
-               "Arkai",
-               "Avado",
-               "Djahhim",
-               "Khankhen",
-               "Porok",
-               "Miras",
-               "Terok",
-               "Empok",
-               "Noragh",
-               "Nuunian",
-               "Soung",
-               "Akhero",
-               "Qozaq",
-               "Kherus",
-               "Axina",
-               "Ghaizas",
-               "Saxha",
-               "Meshu",
-               "Khopesh",
-               "Qitemar",
-               "Vang",
-               "Lugal",
-               "Galla",
-               "Hheka",
-               "Nesut",
-               "Koquon",
-               "Molekh"
-       )
-       
-       private fun randomSouthernDiadochiClan() = when {
-               Random.nextInt(5) == 0 -> southernDiadochiClans.random() + "-" + southernDiadochiClans.random()
-               else -> southernDiadochiClans.random()
-       }
-       
-       private fun randomSouthernDiadochiName(isFemale: Boolean) = (if (isFemale) diadochiFemaleNames else diadochiMaleNames).random() + (if (isFemale && Random.nextBoolean()) " ka-" else " am-") + diadochiMaleNames.random() + " " + randomSouthernDiadochiClan()
-       
-       private val thedishMaleNames = listOf(
-               "Praethoris",
-               "Severus",
-               "Augast",
-               "Dagobar",
-               "Vrankenn",
-               "Kandar",
-               "Kleon",
-               "Glaius",
-               "Karul",
-               "Ylai",
-               "Toval",
-               "Ivon",
-               "Belis",
-               "Jorh",
-               "Svar",
-               "Alaric",
-       )
-       
-       private val thedishFemaleNames = listOf(
-               "Serna",
-               "Veleska",
-               "Ielga",
-               "Glae",
-               "Rova",
-               "Ylia",
-               "Galera",
-               "Nerys",
-               "Veleer",
-               "Karuleyn",
-               "Amberli",
-               "Alysia",
-               "Lenera",
-               "Demeter",
-       )
-       
-       private val thedishSurnames = listOf(
-               "Kassck",
-               "Orsh",
-               "Falk",
-               "Khorr",
-               "Vaskoman",
-               "Vholkazk",
-               "Brekoryn",
-               "Lorus",
-               "Karnas",
-               "Hathar",
-               "Takan",
-               "Pertona",
-               "Tefran",
-               "Arvi",
-               "Galvus",
-               "Voss",
-               "Mandanof",
-               "Ursali",
-               "Vytunn",
-               "Quesrinn",
-       )
-       
-       private fun randomThedishName(isFemale: Boolean) = (if (isFemale) thedishFemaleNames else thedishMaleNames).random() + " " + thedishSurnames.random()
-       
-       private val americanMaleNames = listOf(
-               "George",
-               "John",
-               "Thomas",
-               "James",
-               "Quincy",
-               "Andrew",
-               "Martin",
-               "William",
-               "Henry",
-               "James",
-               "Zachary",
-               "Millard",
-               "Franklin",
-               "Abraham",
-               "Ulysses",
-               "Rutherford",
-               "Chester",
-               "Grover",
-               "Benjamin",
-               "Theodore",
-               "Warren",
-               "Calvin",
-               "Herbert",
-               "Harry",
-               "Dwight",
-               "Lyndon",
-               "Richard",
-               "Dick",
-               "Gerald",
-               "Jimmy",
-               "Ronald",
-               "Donald"
-       )
-       
-       private val americanFemaleNames = listOf(
-               "Martha",
-               "Abigail",
-               "Elizabeth",
-               "Louisa",
-               "Emily",
-               "Sarah",
-               "Anna",
-               "Jane",
-               "Julia",
-               "Margaret",
-               "Harriet",
-               "Mary",
-               "Lucy",
-               "Rose",
-               "Caroline",
-               "Ida",
-               "Helen",
-               "Grace",
-               "Jacqueline",
-               "Thelma",
-               "Eleanor",
-               "Nancy",
-               "Barbara",
-               "Laura",
-               "Melania"
-       )
-       
-       private val americanFamilyNames = listOf(
-               "Knox",
-               "Pickering",
-               "McHenry",
-               "Dexter",
-               "Drawborn",
-               "Eustis",
-               "Armstrong",
-               "Monroe",
-               "Crawford",
-               "Calhoun",
-               "Barbour",
-               "Porter",
-               "Eaton",
-               "Cass",
-               "Poinsett",
-               "Bell",
-               "Forrestal",
-               "Johnson",
-               "Marshall",
-               "Lovett",
-               "Wilson",
-               "McElroy",
-               "McNamara",
-               "Clifford",
-               "Richardson",
-               "Burndt",
-       )
-       
-       private fun randomAmericanName(isFemale: Boolean) = (if (isFemale) americanFemaleNames else americanMaleNames).random() + " " + americanFamilyNames.random()
-       
-       private val hispanicMaleNames = listOf(
-               "Aaron",
-               "Antonio",
-               "Augusto",
-               "Eliseo",
-               "Manuel",
-               "Jose",
-               "Juan",
-               "Miguel",
-               "Rafael",
-               "Raul",
-               "Adriano",
-               "Emilio",
-               "Francisco",
-               "Ignacio",
-               "Marco",
-               "Pablo",
-               "Octavio",
-               "Victor",
-               "Vito",
-               "Valentin"
-       )
-       
-       private val hispanicFemaleNames = listOf(
-               "Maria",
-               "Ana",
-               "Camila",
-               "Eva",
-               "Flora",
-               "Gloria",
-               "Julia",
-               "Marcelina",
-               "Rosalia",
-               "Victoria",
-               "Valentina",
-               "Cecilia",
-               "Francisca",
-               "Aurelia",
-               "Cristina",
-               "Magdalena",
-               "Margarita",
-               "Martina",
-               "Teresa"
-       )
-       
-       private val hispanicFamilyNames = listOf(
-               "Acorda",
-               "Aguirre",
-               "Alzaga",
-               "Arriaga",
-               "Arrieta",
-               "Berroya",
-               "Barahona",
-               "Carranza",
-               "Carriaga",
-               "Elcano",
-               "Elizaga",
-               "Endaya",
-               "Franco",
-               "Garalde",
-               "Ibarra",
-               "Juarez",
-               "Lazarte",
-               "Legarda",
-               "Madariaga",
-               "Medrano",
-               "Narvaez",
-               "Olano",
-               "Ricarte",
-               "Salazar",
-               "Uriarte",
-               "Varona",
-               "Vergar",
-       )
-       
-       private fun randomHispanicName(isFemale: Boolean) = (if (isFemale) hispanicFemaleNames else hispanicMaleNames).random() + " " + hispanicFamilyNames.random()
-       
-       fun randomName(flavor: AdmiralNameFlavor, isFemale: Boolean) = when (flavor) {
-               AdmiralNameFlavor.MECHYRDIA -> randomMechyrdianName(isFemale)
-               AdmiralNameFlavor.TYLA -> randomTylanName(isFemale)
-               AdmiralNameFlavor.CALIBOR -> randomCaliboreseName(isFemale)
-               AdmiralNameFlavor.OLYMPIA -> randomLatinName(isFemale)
-               AdmiralNameFlavor.DUTCH -> randomDutchName(isFemale)
-               AdmiralNameFlavor.NORTHERN_DIADOCHI -> randomNorthernDiadochiName(isFemale)
-               AdmiralNameFlavor.SOUTHERN_DIADOCHI -> randomSouthernDiadochiName(isFemale)
-               AdmiralNameFlavor.FULKREYKK -> randomThedishName(isFemale)
-               AdmiralNameFlavor.AMERICAN -> randomAmericanName(isFemale)
-               AdmiralNameFlavor.HISPANIC_AMERICAN -> randomHispanicName(isFemale)
-       }
-}
diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt
deleted file mode 100644 (file)
index d09ce98..0000000
+++ /dev/null
@@ -1,452 +0,0 @@
-package starshipfights.data.admiralty
-
-import starshipfights.game.Faction
-import starshipfights.game.ShipWeightClass
-import kotlin.random.Random
-
-fun newShipName(faction: Faction, shipWeightClass: ShipWeightClass, existingNames: MutableSet<String>) = generateSequence {
-       nameShip(faction, shipWeightClass)
-}.take(20).dropWhile { it in existingNames }.firstOrNull()?.also { existingNames.add(it) }
-
-private val mechyrdianFrigateNames1 = listOf(
-       "Unconquerable",
-       "Indomitable",
-       "Invincible",
-       "Imperial",
-       "Regal",
-       "Royal",
-       "Imperious",
-       "Honorable",
-       "Defiant",
-       "Eternal",
-       "Infinite",
-       "Dominant",
-       "Divine",
-       "Righteous",
-       "Resplendent",
-       "Protective",
-       "Innocent",
-       "August",
-       "Loyal"
-)
-
-private val mechyrdianFrigateNames2 = listOf(
-       "Faith",
-       "Empire",
-       "Royalty",
-       "Regality",
-       "Honor",
-       "Defiance",
-       "Eternity",
-       "Dominator",
-       "Divinity",
-       "Right",
-       "Righteousness",
-       "Resplendency",
-       "Defender",
-       "Protector",
-       "Innocence",
-       "Victory",
-       "Duty",
-       "Loyalty"
-)
-
-private val mechyrdianCruiserNames1 = listOf(
-       "Defender of",
-       "Protector of",
-       "Shield of",
-       "Sword of",
-       "Champion of",
-       "Hero of",
-       "Salvation of",
-       "Savior of",
-       "Shining Light of",
-       "Righteous Flame of",
-       "Eternal Glory of",
-)
-
-private val mechyrdianCruiserNames2 = listOf(
-       "Mechyrd",
-       "Kaiserswelt",
-       "Tenno no Wakusei",
-       "Nova Roma",
-       "Mont Imperial",
-       "Tyla",
-       "Vensca",
-       "Kaltag",
-       "Languavarth Prime",
-       "Languavarth Secundum",
-       "Elcialot",
-       "Othon",
-       "Starport",
-       "Sacrilegum",
-       "New Constantinople",
-       "Fairhus",
-       "Praxagora",
-       "Karolina",
-       "Kozachnia",
-       "New New Amsterdam",
-       "Mundus Caesaris Divi",
-       "Saiwatta",
-       "Earth"
-)
-
-private val mechyrdianBattleshipNames = listOf(
-       "Kaiser Wilhelm I",
-       "Kaiser Wilhelm II",
-       "Empereur Napoléon I Bonaparte",
-       "Tsar Nikolaj II Romanov",
-       "Seliger Kaiser Karl I von Habsburg",
-       "Emperor Joshua A. Norton I",
-       "Emperor Meiji the Great",
-       "Emperor Jack G. Coleman",
-       "Emperor Trevor C. Neer",
-       "Emperor Connor F. Vance",
-       "Emperor Jean-Bédel Bokassa I",
-       "King Charles XII",
-       "King William I the Conqueror",
-       "King Alfred the Great",
-       "Gustavus Adolphus Magnus Rex",
-       "Queen Victoria",
-       "Kōnstantînos XI Dragásēs Palaiológos",
-       "Ioustinianós I ho Mégas",
-       "Kjarossa Liha Vilakauva",
-       "Kjarossa Tarkona Sovasra",
-       "Great King Kūruš",
-       "Queen Elizabeth II",
-       "Kjarossa Karelka Helasra",
-       "Imperātor Cæsar Dīvī Fīlius Augustus",
-       "Cæsar Nerva Trāiānus",
-       "King Kaleb of Axum"
-)
-
-private fun nameMechyrdianShip(weightClass: ShipWeightClass) = when (weightClass) {
-       ShipWeightClass.ESCORT -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
-       ShipWeightClass.DESTROYER -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
-       ShipWeightClass.CRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
-       ShipWeightClass.BATTLECRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
-       ShipWeightClass.BATTLESHIP -> mechyrdianBattleshipNames.random()
-       ShipWeightClass.BATTLE_BARGE -> mechyrdianBattleshipNames.random()
-       else -> error("Invalid Mechyrdian ship weight!")
-}
-
-private val masraDraetsenFrigateNames1 = listOf(
-       "Murderous",
-       "Hateful",
-       "Heinous",
-       "Pestilent",
-       "Corrupting",
-       "Homicidal",
-       "Deadly",
-       "Primordial",
-       "Painful",
-       "Agonizing",
-       "Spiteful",
-       "Odious",
-       "Miserating",
-       "Damned",
-       "Condemned",
-       "Hellish",
-       "Dark",
-       "Impious",
-       "Unfaithful",
-       "Abyssal",
-       "Furious",
-       "Vengeful",
-       "Spiritous"
-)
-
-private val masraDraetsenFrigateNames2 = listOf(
-       "Murder",
-       "Hate",
-       "Hatred",
-       "Pestilence",
-       "Corruption",
-       "Homicide",
-       "Massacre",
-       "Death",
-       "Agony",
-       "Pain",
-       "Suffering",
-       "Spite",
-       "Misery",
-       "Damnation",
-       "Hell",
-       "Darkness",
-       "Impiety",
-       "Faithlessness",
-       "Abyss",
-       "Fury",
-       "Vengeance",
-       "Spirit"
-)
-
-private val masraDraetsenCruiserNames1 = listOf(
-       "Despoiler of",
-       "Desecrator of",
-       "Desolator of",
-       "Destroyer of",
-       "Executioner of",
-       "Pillager of",
-       "Villain of",
-       "Great Devil of",
-       "Infidelity of",
-       "Incineration of",
-       "Immolation of",
-       "Crucifixion of",
-       "Unending Darkness of",
-)
-
-private val masraDraetsenCruiserNames2 = listOf(
-       // Diadochi space
-       "Eskhaton",
-       "Terminus",
-       "Tychiphage",
-       "Magaddu",
-       "Ghattusha",
-       "Three Suns",
-       "RB-5354",
-       "VT-3072",
-       "Siegsstern",
-       "Atzalstadt",
-       "Apex",
-       "Summit",
-       // Lyudareykk and Isarnareykk
-       "Vion Kann",
-       "Kasr Karul",
-       "Vladizapad",
-       // Chaebodes Star Empire
-       "Ultima Thule",
-       "Prenovez",
-       // Calibor and Vescar sectors
-       "Letum Angelorum",
-       "Pharsalus",
-       "Eutopia",
-       // Ferthlon and Olympia sectors
-       "Ferthlon Primus",
-       "Ferthlon Secundus",
-       "Nova Roma",
-       "Mont Imperial",
-)
-
-private const val masraDraetsenColossusName = "Boukephalas"
-
-private fun nameMasraDraetsenShip(weightClass: ShipWeightClass) = when (weightClass) {
-       ShipWeightClass.ESCORT -> "${masraDraetsenFrigateNames1.random()} ${masraDraetsenFrigateNames2.random()}"
-       ShipWeightClass.DESTROYER -> "${masraDraetsenFrigateNames1.random()} ${masraDraetsenFrigateNames2.random()}"
-       ShipWeightClass.CRUISER -> "${masraDraetsenCruiserNames1.random()} ${masraDraetsenCruiserNames2.random()}"
-       ShipWeightClass.GRAND_CRUISER -> "${masraDraetsenCruiserNames1.random()} ${masraDraetsenCruiserNames2.random()}"
-       ShipWeightClass.COLOSSUS -> masraDraetsenColossusName
-       else -> error("Invalid Masra Draetsen ship weight!")
-}
-
-private enum class LatinNounForm {
-       MAS_SG,
-       FEM_SG,
-       NEU_SG,
-       MAS_PL,
-       FEM_PL,
-       NEU_PL,
-}
-
-private data class LatinNoun(
-       val noun: String,
-       val form: LatinNounForm
-)
-
-private data class LatinAdjective(
-       val masculineSingular: String,
-       val feminineSingular: String,
-       val neuterSingular: String,
-       val masculinePlural: String,
-       val femininePlural: String,
-       val neuterPlural: String,
-) {
-       fun get(form: LatinNounForm) = when (form) {
-               LatinNounForm.MAS_SG -> masculineSingular
-               LatinNounForm.FEM_SG -> feminineSingular
-               LatinNounForm.NEU_SG -> neuterSingular
-               LatinNounForm.MAS_PL -> masculinePlural
-               LatinNounForm.FEM_PL -> femininePlural
-               LatinNounForm.NEU_PL -> neuterPlural
-       }
-}
-
-private infix fun LatinNoun.describedBy(adjective: LatinAdjective) = "$noun ${adjective.get(form)}"
-
-private fun felinaeFelicesEscortShipName() = "ES-" + (1000..9999).random().toString()
-
-private val felinaeFelicesLineShipNames1 = listOf(
-       LatinNoun("Aevum", LatinNounForm.NEU_SG),
-       LatinNoun("Aquila", LatinNounForm.FEM_SG),
-       LatinNoun("Argonauta", LatinNounForm.MAS_SG),
-       LatinNoun("Cattus", LatinNounForm.MAS_SG),
-       LatinNoun("Daemon", LatinNounForm.MAS_SG),
-       LatinNoun("Divitia", LatinNounForm.FEM_SG),
-       LatinNoun("Feles", LatinNounForm.FEM_SG),
-       LatinNoun("Imperium", LatinNounForm.NEU_SG),
-       LatinNoun("Ius", LatinNounForm.NEU_SG),
-       LatinNoun("Iustitia", LatinNounForm.FEM_SG),
-       LatinNoun("Leo", LatinNounForm.MAS_SG),
-       LatinNoun("Leopardus", LatinNounForm.MAS_SG),
-       LatinNoun("Lynx", LatinNounForm.FEM_SG),
-       LatinNoun("Panthera", LatinNounForm.FEM_SG),
-       LatinNoun("Salvator", LatinNounForm.MAS_SG),
-       LatinNoun("Scelus", LatinNounForm.NEU_SG),
-       LatinNoun("Tigris", LatinNounForm.MAS_SG),
-)
-
-private val felinaeFelicesLineShipNames2 = listOf(
-       LatinAdjective("Animosus", "Animosa", "Animosum", "Animosi", "Animosae", "Animosa"),
-       LatinAdjective("Ardens", "Ardens", "Ardens", "Ardentes", "Ardentes", "Ardentia"),
-       LatinAdjective("Audax", "Audax", "Audax", "Audaces", "Audaces", "Audacia"),
-       LatinAdjective("Astutus", "Astuta", "Astutum", "Astuti", "Astutae", "Astuta"),
-       LatinAdjective("Calidus", "Calida", "Calidum", "Calidi", "Calidae", "Calida"),
-       LatinAdjective("Ferox", "Ferox", "Ferox", "Feroces", "Feroces", "Ferocia"),
-       LatinAdjective("Fortis", "Fortis", "Forte", "Fortes", "Fortes", "Fortia"),
-       LatinAdjective("Fugax", "Fugax", "Fugax", "Fugaces", "Fugaces", "Fugacia"),
-       LatinAdjective("Indomitus", "Indomita", "Indomitum", "Indomiti", "Indomitae", "Indomita"),
-       LatinAdjective("Intrepidus", "Intrepida", "Intrepidum", "Intrepidi", "Intrepidae", "Intrepida"),
-       LatinAdjective("Pervicax", "Pervicax", "Pervicax", "Pervicaces", "Pervicaces", "Pervicacia"),
-       LatinAdjective("Sagax", "Sagax", "Sagax", "Sagaces", "Sagaces", "Sagacia"),
-       LatinAdjective("Superbus", "Superba", "Superbum", "Superbi", "Superbae", "Superba"),
-       LatinAdjective("Trux", "Trux", "Trux", "Truces", "Truces", "Trucia"),
-)
-
-private fun nameFelinaeFelicesShip(weightClass: ShipWeightClass) = when (weightClass) {
-       ShipWeightClass.FF_ESCORT -> felinaeFelicesEscortShipName()
-       ShipWeightClass.FF_DESTROYER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
-       ShipWeightClass.FF_CRUISER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
-       ShipWeightClass.FF_BATTLECRUISER -> felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random()
-       ShipWeightClass.FF_BATTLESHIP -> if (Random.nextDouble() < 0.01) "Big Floppa" else (felinaeFelicesLineShipNames1.random() describedBy felinaeFelicesLineShipNames2.random())
-       else -> error("Invalid Felinae Felices ship weight!")
-}
-
-private val isarnareykkShipNames = listOf(
-       "Professional with Standards",
-       "Online Game Cheater",
-       "Actually Made of Antimatter",
-       "Chucklehead",
-       "Guns Strapped to an Engine",
-       "Unidentified Comet",
-       "Deep Space Encounter",
-       "The Goggles Do Nothing",
-       "Sensor Error",
-       "ERROR SHIP NAME NOT FOUND",
-       "0x426F6174",
-       "Börgenkub",
-       "Instant Death",
-       "Assume The Position",
-       "Negative Space Wedgie",
-       "Tea, Earl Grey, Hot",
-       "There's Coffee In That Nebula",
-       "SPEHSS MEHREENS",
-       "Inconspicuous Asteroid",
-       "Inflatable Toy Ship",
-       "HELP TRAPPED IN SHIP FACTORY",
-       "Illegal Meme Dealer",
-       "Reverse the Polarity!",
-       "Send Your Bank Info To Win 10,000 Marks",
-       "STOP CALLING ABOUT MY STARSHIP WARRANTY",
-       "Somebody Once Told Me...",
-       "Praethoris Khorr Gaming",
-)
-
-private fun nameIsarnareykskShip() = isarnareykkShipNames.random()
-
-private val vestigiumShipNames = listOf(
-       // NAMED AFTER SPACE SHUTTLES
-       "Enterprise", // OV-101
-       "Columbia", // OV-102
-       "Discovery", // OV-103
-       "Atlantis", // OV-104
-       "Endeavor", // OV-105
-       "Conqueror", // OV-106
-       "Homeland", // OV-107
-       "Augustus", // OV-108
-       "Avenger", // OV-109
-       "Protector", // OV-110
-       
-       // NAMED AFTER HISTORICAL SHIPS
-       "Yorktown",
-       "Lexington",
-       "Ranger",
-       "Hornet",
-       "Wasp",
-       "Antares",
-       "Belfast",
-       // NAMED AFTER PLACES
-       "Akron",
-       "Hudson",
-       "Cleveland",
-       "Baltimore",
-       "Bel Air",
-       "Cedar Rapids",
-       "McHenry",
-       "Rochester",
-       "Cuyahoga Valley",
-       "Catonsville",
-       "Ocean City",
-       "Philadelphia",
-       "Somerset",
-       "Pittsburgh",
-       
-       "Las Vegas",
-       "Reno",
-       "Boulder City",
-       "Goodsprings",
-       "Nipton",
-       "Primm",
-       "Nellis",
-       "Fortification Hill",
-       "McCarran",
-       "Fremont",
-       
-       // NAMED AFTER SPACE PROBES
-       "Voyager",
-       "Juno",
-       "Cassini",
-       "Hubble",
-       "Huygens",
-       "Pioneer",
-       
-       // NAMED AFTER PEOPLE
-       // Founding Fathers
-       "George Washington",
-       "Thomas Jefferson",
-       "John Adams",
-       "Alexander Hamilton",
-       "James Madison",
-       // US Presidents
-       "Andrew Jackson",
-       "Abraham Lincoln",
-       "Theodore Roosevelt",
-       "Calvin Coolidge",
-       "Dwight Eisenhower",
-       "Richard Nixon",
-       "Ronald Reagan",
-       "Donald Trump",
-       "Ron DeSantis",
-       "Gary Martison",
-       // IS Emperors
-       "Jack Coleman",
-       "Trevor Neer",
-       "Hadrey Trevison",
-       "Dio Audrey",
-       "Connor Vance",
-       // Vestigium Leaders
-       "Thomas Blackrock",
-       "Philip Mack",
-       "Ilya Korochenko"
-)
-
-private fun nameAmericanShip() = vestigiumShipNames.random()
-
-fun nameShip(faction: Faction, weightClass: ShipWeightClass): String = when (faction) {
-       Faction.MECHYRDIA -> nameMechyrdianShip(weightClass)
-       Faction.NDRC -> nameMechyrdianShip(weightClass)
-       Faction.MASRA_DRAETSEN -> nameMasraDraetsenShip(weightClass)
-       Faction.FELINAE_FELICES -> nameFelinaeFelicesShip(weightClass)
-       Faction.ISARNAREYKK -> nameIsarnareykskShip()
-       Faction.VESTIGIUM -> nameAmericanShip()
-}
index ee618551ef1b60e2aaceab1537ce0405b0450aa1..40bcce5252da706e7b69d821b2853ad127825239 100644 (file)
@@ -83,6 +83,7 @@ suspend fun generateTrainingInitialState(playerInfo: InGameAdmiral, enemyFaction
                        faction = aiAdmiral.faction,
                        rank = aiAdmiral.rank
                ),
-               battleInfo = battleInfo
+               battleInfo = battleInfo,
+               subplots = generateSubplots(battleInfo.size, GlobalSide.HOST)
        )
 }
index ff96b3f5be6ae83811c86b4d0a174bf0edec2bf5..a63d02b8671afdecf228e631dfd1cc3a2e642413 100644 (file)
@@ -32,7 +32,8 @@ object GameManager {
                        start = generateGameStart(hostInfo, guestInfo, battleInfo),
                        hostInfo = hostInfo,
                        guestInfo = guestInfo,
-                       battleInfo = battleInfo
+                       battleInfo = battleInfo,
+                       subplots = generateSubplots(battleInfo.size, GlobalSide.HOST) + generateSubplots(battleInfo.size, GlobalSide.GUEST)
                )
                
                val session = GameSession(gameState)